feat(schematic): eeschema WASM build + e2e harness
This commit is contained in:
parent
31ff88ee9e
commit
730147d690
19 changed files with 3945 additions and 414 deletions
|
|
@ -1,8 +1,13 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Build KiCad WASM inside Docker container, then apply asyncify on host
|
# Build a KiCad editor (pcbnew or eeschema) inside Docker, then run asyncify
|
||||||
|
# and friends on the host.
|
||||||
# Redirect all output to a log file (re-execs script with redirection)
|
#
|
||||||
source "$(dirname "$0")/../scripts/common/logging.sh"
|
# Usage:
|
||||||
|
# ./docker/build.sh # builds pcbnew (default)
|
||||||
|
# ./docker/build.sh pcbnew # explicit
|
||||||
|
# ./docker/build.sh eeschema # builds the schematic editor
|
||||||
|
# ./docker/build.sh all # builds both, sequentially
|
||||||
|
# ./docker/build.sh <app> -j 8 ... # any extra args are forwarded to build-*.sh
|
||||||
#
|
#
|
||||||
# The build is split into two phases:
|
# The build is split into two phases:
|
||||||
# 1. Docker: Compile KiCad to WASM (without asyncify)
|
# 1. Docker: Compile KiCad to WASM (without asyncify)
|
||||||
|
|
@ -10,14 +15,34 @@ source "$(dirname "$0")/../scripts/common/logging.sh"
|
||||||
#
|
#
|
||||||
# Binaryen is downloaded automatically - no prerequisites needed.
|
# Binaryen is downloaded automatically - no prerequisites needed.
|
||||||
|
|
||||||
|
# Redirect all output to a log file (re-execs script with redirection)
|
||||||
|
source "$(dirname "$0")/../scripts/common/logging.sh"
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# First positional arg is the app name; everything else is forwarded to build-*.sh.
|
||||||
|
APP_NAME=""
|
||||||
|
if [[ $# -gt 0 ]] && [[ "$1" != -* ]]; then
|
||||||
|
APP_NAME="$1"
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
APP_NAME="${APP_NAME:-pcbnew}"
|
||||||
|
|
||||||
|
case "$APP_NAME" in
|
||||||
|
pcbnew|eeschema|all) ;;
|
||||||
|
*)
|
||||||
|
echo "Error: unknown app '$APP_NAME' (expected: pcbnew | eeschema | all)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Use branch name as Docker Compose project name for isolated containers/volumes
|
# Use branch name as Docker Compose project name for isolated containers/volumes
|
||||||
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD | tr '/' '-' | tr '[:upper:]' '[:lower:]')
|
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD | tr '/' '-' | tr '[:upper:]' '[:lower:]')
|
||||||
export COMPOSE_PROJECT_NAME="kicad-wasm-${BRANCH_NAME}"
|
export COMPOSE_PROJECT_NAME="kicad-wasm-${BRANCH_NAME}"
|
||||||
echo "Using Docker project: ${COMPOSE_PROJECT_NAME}"
|
echo "Using Docker project: ${COMPOSE_PROJECT_NAME}"
|
||||||
|
echo "Building app: ${APP_NAME}"
|
||||||
|
|
||||||
# Add -j 10 by default if no -j flag is given
|
# Add -j 10 by default if no -j flag is given
|
||||||
ARGS=("$@")
|
ARGS=("$@")
|
||||||
|
|
@ -62,34 +87,45 @@ if [ $sync_rc -ne 0 ] && [ $sync_rc -ne 24 ]; then
|
||||||
echo "ERROR: source sync failed after retries (exit ${sync_rc})"; exit 1
|
echo "ERROR: source sync failed after retries (exit ${sync_rc})"; exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run build command (without asyncify - handled on host due to memory requirements)
|
# Build one app: compile in container, then run host-side post-processing.
|
||||||
# -e EMSDK=/emsdk: `docker compose exec` bypasses the entrypoint that sources
|
build_app() {
|
||||||
|
local app="$1"
|
||||||
|
echo ""
|
||||||
|
echo "=== Building ${app} ==="
|
||||||
|
|
||||||
|
# Run build command (without asyncify - handled on host due to memory requirements)
|
||||||
|
# -e EMSDK=/emsdk: `docker compose exec` bypasses the entrypoint that sources
|
||||||
# emsdk_env.sh, so the build shell would lack emcc/embuilder on PATH. Setting
|
# emsdk_env.sh, so the build shell would lack emcc/embuilder on PATH. Setting
|
||||||
# EMSDK lets scripts/common/env.sh source /emsdk/emsdk_env.sh and activate the toolchain.
|
# EMSDK lets scripts/common/env.sh source /emsdk/emsdk_env.sh and activate the toolchain.
|
||||||
docker compose -f docker/docker-compose.yml exec -e EMSDK=/emsdk kicad-wasm-builder \
|
docker compose -f docker/docker-compose.yml exec -e EMSDK=/emsdkkicad-wasm-builder \
|
||||||
/workspace/scripts/kicad/build-pcbnew.sh "${ARGS[@]}"
|
"/workspace/scripts/kicad/build-${app}.sh" "${ARGS[@]}"
|
||||||
|
|
||||||
# Copy output to host-accessible directory
|
# Copy output to host-accessible directory.
|
||||||
# Note: pcbnew.wasm.debug.wasm contains DWARF debug info (generated with -gseparate-dwarf)
|
# ${app}.wasm.debug.wasm contains DWARF debug info (when built with -gseparate-dwarf).
|
||||||
echo "Copying build output to ./output/..."
|
echo "Copying ${app} build output to ./output/..."
|
||||||
docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \
|
docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \
|
||||||
bash -c "mkdir -p /workspace/output && \
|
bash -c "mkdir -p /workspace/output && \
|
||||||
cp /workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.{js,wasm,wasm.debug.wasm,wasm.map,worker.js} /workspace/output/ 2>/dev/null || \
|
cp /workspace/build-wasm/kicad-${app}/${app}/${app}.{js,wasm,wasm.debug.wasm,wasm.map,worker.js} /workspace/output/ 2>/dev/null || \
|
||||||
cp /workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.{js,wasm} /workspace/output/; \
|
cp /workspace/build-wasm/kicad-${app}/${app}/${app}.{js,wasm} /workspace/output/; \
|
||||||
cp /workspace/build-wasm/kicad-pcbnew/resources/images.tar.gz /workspace/output/ 2>/dev/null || true; \
|
cp /workspace/build-wasm/kicad-${app}/resources/images.tar.gz /workspace/output/ 2>/dev/null || true; \
|
||||||
cp /workspace/build-wasm/wxwidgets/build/wasm/wx.js /workspace/output/ 2>/dev/null || true"
|
cp /workspace/build-wasm/wxwidgets/build/wasm/wx.js /workspace/output/ 2>/dev/null || true"
|
||||||
|
|
||||||
# Inject dynCall shims into pcbnew.js
|
# Inject dynCall shims (fixes "dynCall_* is not defined" errors in Emscripten 4.x)
|
||||||
# This fixes "dynCall_* is not defined" errors in Emscripten 4.x
|
./scripts/common/inject-dyncall-shims.sh "output/${app}.js"
|
||||||
./scripts/common/inject-dyncall-shims.sh output/pcbnew.js
|
|
||||||
|
|
||||||
# Apply wasm-emscripten-finalize on host (skipped in Docker due to memory limits)
|
# Apply wasm-emscripten-finalize on host (skipped in Docker due to memory limits)
|
||||||
# This is done on the host because finalize with DWARF needs significant RAM
|
./scripts/common/apply-finalize.sh "output/${app}.wasm" "output/${app}.wasm"
|
||||||
./scripts/common/apply-finalize.sh output/pcbnew.wasm output/pcbnew.wasm
|
|
||||||
|
|
||||||
# Apply asyncify transformation on host
|
# Apply asyncify transformation on host
|
||||||
# This is done on the host because wasm-opt --asyncify needs significant RAM
|
./scripts/common/apply-asyncify.sh "output/${app}.wasm" "output/${app}.wasm"
|
||||||
./scripts/common/apply-asyncify.sh output/pcbnew.wasm output/pcbnew.wasm
|
}
|
||||||
|
|
||||||
|
if [[ "${APP_NAME}" == "all" ]]; then
|
||||||
|
build_app pcbnew
|
||||||
|
build_app eeschema
|
||||||
|
else
|
||||||
|
build_app "${APP_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Build complete. Output files in ./output/"
|
echo "Build complete. Output files in ./output/"
|
||||||
|
|
|
||||||
54
features/schematic/0001-eeschema-iface-stubs.md
Normal file
54
features/schematic/0001-eeschema-iface-stubs.md
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# 0001 — eeschema IFACE sub-frame stubs (WASM)
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Eeschema's `IFACE::CreateKiWindow` in [kicad/eeschema/eeschema.cpp](../../kicad/eeschema/eeschema.cpp) dispatches `FRAME_T` ids to four frame constructors:
|
||||||
|
|
||||||
|
- `FRAME_SCH` → `SCH_EDIT_FRAME` (the schematic editor — what we want)
|
||||||
|
- `FRAME_SCH_SYMBOL_EDITOR` → `SYMBOL_EDIT_FRAME` (symbol library editor)
|
||||||
|
- `FRAME_SCH_VIEWER` → `SYMBOL_VIEWER_FRAME` (symbol library viewer)
|
||||||
|
- `FRAME_SYMBOL_CHOOSER` → `SYMBOL_CHOOSER_FRAME` (symbol chooser dialog)
|
||||||
|
- `FRAME_SIMULATOR` → `SIMULATOR_FRAME` (already wrapped in try/catch — no patch needed)
|
||||||
|
|
||||||
|
The MVP scope for the WASM port is "empty schematic editor + draw a wire", so the three sub-frames (symbol editor / viewer / chooser) are out of scope. They are non-trivial to support in the browser — they need bundled symbol libraries, FS access for `.kicad_sym` lookup, and full dialog plumbing.
|
||||||
|
|
||||||
|
Compiling them out at the source level (removing `EESCHEMA_LIBEDIT_SRCS` from the build) cascades into many CMakeLists.txt and symbol-chooser sources; cleaner to keep the sources compiled and just refuse to instantiate the frames at the IFACE switch.
|
||||||
|
|
||||||
|
## What changed
|
||||||
|
|
||||||
|
`kicad/eeschema/eeschema.cpp`, the three sub-frame cases inside `IFACE::CreateKiWindow` are now guarded:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
case FRAME_SCH_SYMBOL_EDITOR:
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
return nullptr;
|
||||||
|
#else
|
||||||
|
return new SYMBOL_EDIT_FRAME( aKiway, aParent );
|
||||||
|
#endif
|
||||||
|
|
||||||
|
case FRAME_SCH_VIEWER:
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
return nullptr;
|
||||||
|
#else
|
||||||
|
return new SYMBOL_VIEWER_FRAME( aKiway, aParent );
|
||||||
|
#endif
|
||||||
|
|
||||||
|
case FRAME_SYMBOL_CHOOSER:
|
||||||
|
{
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
return nullptr;
|
||||||
|
#else
|
||||||
|
// … existing body …
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`FRAME_SIMULATOR` is untouched — its existing `try/catch (SIMULATOR_INIT_ERR&)` block catches the ngspice init failure that our header stub eventually triggers, and returns nullptr the same way.
|
||||||
|
|
||||||
|
## How to apply
|
||||||
|
|
||||||
|
Captured under `features/schematic/kicad.patch` via `./scripts/create-feature-patches.sh schematic` once the build is green.
|
||||||
|
|
||||||
|
## Tested by
|
||||||
|
|
||||||
|
`tests/kicad/eeschema.spec.ts` — wizard completion + `Draw Wires` tool test. Neither test path exercises the stubbed-out frames.
|
||||||
432
features/schematic/kicad.patch
Normal file
432
features/schematic/kicad.patch
Normal file
|
|
@ -0,0 +1,432 @@
|
||||||
|
diff --git a/eeschema/CMakeLists.txt b/eeschema/CMakeLists.txt
|
||||||
|
index 1be2c291d1..e07d05e6e7 100644
|
||||||
|
--- a/eeschema/CMakeLists.txt
|
||||||
|
+++ b/eeschema/CMakeLists.txt
|
||||||
|
@@ -60,35 +60,48 @@ set( EESCHEMA_SCH_IO
|
||||||
|
|
||||||
|
# HTTP IO plugin
|
||||||
|
sch_io/http_lib/sch_io_http_lib.cpp
|
||||||
|
+ )
|
||||||
|
|
||||||
|
- # Database IO plugin
|
||||||
|
- sch_io/database/sch_io_database.cpp
|
||||||
|
-
|
||||||
|
- # Eagle IO plugin
|
||||||
|
- sch_io/eagle/sch_io_eagle.cpp
|
||||||
|
-
|
||||||
|
- # Altium IO plugin
|
||||||
|
- sch_io/altium/altium_parser_sch.cpp
|
||||||
|
- sch_io/altium/sch_io_altium.cpp
|
||||||
|
-
|
||||||
|
- # Cadstar IO plugin
|
||||||
|
- sch_io/cadstar/cadstar_sch_archive_loader.cpp
|
||||||
|
- sch_io/cadstar/cadstar_sch_archive_parser.cpp
|
||||||
|
- sch_io/cadstar/sch_io_cadstar_archive.cpp
|
||||||
|
-
|
||||||
|
- # LTSpice IO plugin
|
||||||
|
- sch_io/ltspice/ltspice_schematic.cpp
|
||||||
|
- sch_io/ltspice/sch_io_ltspice.cpp
|
||||||
|
- sch_io/ltspice/sch_io_ltspice_parser.cpp
|
||||||
|
-
|
||||||
|
- # EasyEDA IO plugin
|
||||||
|
- sch_io/easyeda/sch_easyeda_parser.cpp
|
||||||
|
- sch_io/easyeda/sch_io_easyeda.cpp
|
||||||
|
-
|
||||||
|
- # EasyEDA Pro IO plugin
|
||||||
|
- sch_io/easyedapro/sch_easyedapro_parser.cpp
|
||||||
|
- sch_io/easyedapro/sch_io_easyedapro.cpp
|
||||||
|
+# Third-party importers and database plugin are not built for WASM:
|
||||||
|
+# - Database needs nanodbc/ODBC (common/database/database_connection.cpp
|
||||||
|
+# is also gated on NOT EMSCRIPTEN in common/CMakeLists.txt).
|
||||||
|
+# - Altium pulls in pcbnew/pcb_io/altium common code which pcbnew itself
|
||||||
|
+# excludes for WASM (third-party uses std::basic_string<unsigned short>
|
||||||
|
+# that the current Emscripten libc++ rejects).
|
||||||
|
+# - Eagle/Cadstar/LTspice/EasyEDA are cross-tool importers we don't need
|
||||||
|
+# for the MVP and they bring transitive incompatible dependencies in.
|
||||||
|
+# sch_io_mgr.cpp's factory cases are guarded with __EMSCRIPTEN__ to match.
|
||||||
|
+if( NOT EMSCRIPTEN )
|
||||||
|
+ list( APPEND EESCHEMA_SCH_IO
|
||||||
|
+ # Database IO plugin
|
||||||
|
+ sch_io/database/sch_io_database.cpp
|
||||||
|
+
|
||||||
|
+ # Eagle IO plugin
|
||||||
|
+ sch_io/eagle/sch_io_eagle.cpp
|
||||||
|
+
|
||||||
|
+ # Altium IO plugin
|
||||||
|
+ sch_io/altium/altium_parser_sch.cpp
|
||||||
|
+ sch_io/altium/sch_io_altium.cpp
|
||||||
|
+
|
||||||
|
+ # Cadstar IO plugin
|
||||||
|
+ sch_io/cadstar/cadstar_sch_archive_loader.cpp
|
||||||
|
+ sch_io/cadstar/cadstar_sch_archive_parser.cpp
|
||||||
|
+ sch_io/cadstar/sch_io_cadstar_archive.cpp
|
||||||
|
+
|
||||||
|
+ # LTSpice IO plugin
|
||||||
|
+ sch_io/ltspice/ltspice_schematic.cpp
|
||||||
|
+ sch_io/ltspice/sch_io_ltspice.cpp
|
||||||
|
+ sch_io/ltspice/sch_io_ltspice_parser.cpp
|
||||||
|
+
|
||||||
|
+ # EasyEDA IO plugin
|
||||||
|
+ sch_io/easyeda/sch_easyeda_parser.cpp
|
||||||
|
+ sch_io/easyeda/sch_io_easyeda.cpp
|
||||||
|
+
|
||||||
|
+ # EasyEDA Pro IO plugin
|
||||||
|
+ sch_io/easyedapro/sch_easyedapro_parser.cpp
|
||||||
|
+ sch_io/easyedapro/sch_io_easyedapro.cpp
|
||||||
|
)
|
||||||
|
+endif()
|
||||||
|
|
||||||
|
set( EESCHEMA_DLGS
|
||||||
|
dialogs/dialog_annotate.cpp
|
||||||
|
@@ -276,6 +289,22 @@ set( EESCHEMA_SIM_SRCS
|
||||||
|
widgets/tuner_slider_base.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
+# WASM: the four largest BSIM/SOI/HSIM data initializer functions exceed the
|
||||||
|
+# V8/SpiderMonkey limit on locals per function ("too many locals") once
|
||||||
|
+# compiled. Replace them with empty stubs — the simulator UI is unreachable
|
||||||
|
+# in WASM anyway (see wasm/stubs/ngspice/sharedspice.h).
|
||||||
|
+if( EMSCRIPTEN )
|
||||||
|
+ list( REMOVE_ITEM EESCHEMA_SIM_SRCS
|
||||||
|
+ sim/sim_model_ngspice_data_bsim4.cpp
|
||||||
|
+ sim/sim_model_ngspice_data_b3soi.cpp
|
||||||
|
+ sim/sim_model_ngspice_data_b4soi.cpp
|
||||||
|
+ sim/sim_model_ngspice_data_hsim.cpp
|
||||||
|
+ )
|
||||||
|
+ list( APPEND EESCHEMA_SIM_SRCS
|
||||||
|
+ ${CMAKE_SOURCE_DIR}/../wasm/stubs/eeschema_ngspice_data_stubs.cpp
|
||||||
|
+ )
|
||||||
|
+endif()
|
||||||
|
+
|
||||||
|
set( EESCHEMA_WIDGETS
|
||||||
|
widgets/hierarchy_pane.cpp
|
||||||
|
widgets/panel_sch_selection_filter_base.cpp
|
||||||
|
@@ -561,14 +590,25 @@ add_executable( eeschema WIN32 MACOSX_BUNDLE
|
||||||
|
${EESCHEMA_RESOURCES}
|
||||||
|
)
|
||||||
|
|
||||||
|
-set_source_files_properties( ${CMAKE_SOURCE_DIR}/common/single_top.cpp PROPERTIES
|
||||||
|
- COMPILE_DEFINITIONS "TOP_FRAME=FRAME_SCH;PGM_DATA_FILE_EXT=\"kicad_sch\";BUILD_KIWAY_DLL"
|
||||||
|
- )
|
||||||
|
+if( EMSCRIPTEN )
|
||||||
|
+ # WASM: Static kiface linking - don't define BUILD_KIWAY_DLL
|
||||||
|
+ set_source_files_properties( ${CMAKE_SOURCE_DIR}/common/single_top.cpp PROPERTIES
|
||||||
|
+ COMPILE_DEFINITIONS "TOP_FRAME=FRAME_SCH;PGM_DATA_FILE_EXT=\"kicad_sch\""
|
||||||
|
+ )
|
||||||
|
+else()
|
||||||
|
+ set_source_files_properties( ${CMAKE_SOURCE_DIR}/common/single_top.cpp PROPERTIES
|
||||||
|
+ COMPILE_DEFINITIONS "TOP_FRAME=FRAME_SCH;PGM_DATA_FILE_EXT=\"kicad_sch\";BUILD_KIWAY_DLL"
|
||||||
|
+ )
|
||||||
|
+endif()
|
||||||
|
|
||||||
|
-target_link_libraries( eeschema
|
||||||
|
- kicommon
|
||||||
|
- ${wxWidgets_LIBRARIES}
|
||||||
|
- )
|
||||||
|
+if( NOT EMSCRIPTEN )
|
||||||
|
+ # Native: minimal link, kiface loaded dynamically
|
||||||
|
+ target_link_libraries( eeschema
|
||||||
|
+ kicommon
|
||||||
|
+ ${wxWidgets_LIBRARIES}
|
||||||
|
+ )
|
||||||
|
+endif()
|
||||||
|
+# WASM linking is done after EESCHEMA_KIFACE_LIBRARIES is defined
|
||||||
|
|
||||||
|
# the main Eeschema program, in DSO form.
|
||||||
|
add_library( eeschema_kiface_objects OBJECT
|
||||||
|
@@ -577,6 +617,15 @@ add_library( eeschema_kiface_objects OBJECT
|
||||||
|
${EESCHEMA_COMMON_SRCS}
|
||||||
|
)
|
||||||
|
|
||||||
|
+# WASM: Bring eeschema.cpp (the KIFACE entry point) and stub sources into the
|
||||||
|
+# kiface_objects translation unit so static linking finds KIFACE_GETTER.
|
||||||
|
+if( EMSCRIPTEN )
|
||||||
|
+ target_sources( eeschema_kiface_objects PRIVATE
|
||||||
|
+ eeschema.cpp
|
||||||
|
+ ${CMAKE_SOURCE_DIR}/../wasm/stubs/eeschema_frame_stub.cpp
|
||||||
|
+ )
|
||||||
|
+endif()
|
||||||
|
+
|
||||||
|
if( KICAD_USE_PCH )
|
||||||
|
target_precompile_headers( eeschema_kiface_objects
|
||||||
|
PRIVATE
|
||||||
|
@@ -612,21 +661,49 @@ target_link_libraries( eeschema_kiface_objects PUBLIC eeschema_navlib)
|
||||||
|
|
||||||
|
add_dependencies( eeschema_kiface_objects eeschema_navlib )
|
||||||
|
|
||||||
|
-add_library( eeschema_kiface MODULE
|
||||||
|
- eeschema.cpp
|
||||||
|
+if( EMSCRIPTEN )
|
||||||
|
+ # WASM: empty MODULE - eeschema.cpp was hoisted into kiface_objects above
|
||||||
|
+ # so the executable can link KIFACE_GETTER statically. The empty MODULE
|
||||||
|
+ # target is kept so post-build steps that reference it still resolve.
|
||||||
|
+ add_library( eeschema_kiface MODULE )
|
||||||
|
+else()
|
||||||
|
+ add_library( eeschema_kiface MODULE
|
||||||
|
+ eeschema.cpp
|
||||||
|
+ )
|
||||||
|
+endif()
|
||||||
|
+
|
||||||
|
+set( EESCHEMA_KIFACE_LIBRARIES
|
||||||
|
+ eeschema_kiface_objects
|
||||||
|
+ common
|
||||||
|
+ eeschema_navlib
|
||||||
|
+ kicommon
|
||||||
|
+ kiplatform
|
||||||
|
+ gal
|
||||||
|
+ scripting
|
||||||
|
+ sexpr
|
||||||
|
+ core
|
||||||
|
+ markdown_lib
|
||||||
|
+ ${wxWidgets_LIBRARIES}
|
||||||
|
+ ${NGSPICE_LIBRARY}
|
||||||
|
+ Boost::headers
|
||||||
|
)
|
||||||
|
|
||||||
|
+# WASM: Link kiface objects directly into eeschema executable (static linking)
|
||||||
|
+if( EMSCRIPTEN )
|
||||||
|
+ target_link_libraries( eeschema
|
||||||
|
+ PRIVATE
|
||||||
|
+ ${EESCHEMA_KIFACE_LIBRARIES}
|
||||||
|
+ )
|
||||||
|
+ # WASM: Allow multiple definitions of symbols that exist in both
|
||||||
|
+ # KiCad and wxWidgets (matches the pcbnew WASM pattern).
|
||||||
|
+ target_link_options( eeschema PRIVATE
|
||||||
|
+ "LINKER:--allow-multiple-definition"
|
||||||
|
+ )
|
||||||
|
+endif()
|
||||||
|
+
|
||||||
|
target_link_libraries( eeschema_kiface
|
||||||
|
PRIVATE
|
||||||
|
- common
|
||||||
|
- eeschema_kiface_objects
|
||||||
|
- markdown_lib
|
||||||
|
- scripting
|
||||||
|
- sexpr
|
||||||
|
- core
|
||||||
|
- Boost::headers
|
||||||
|
- ${wxWidgets_LIBRARIES}
|
||||||
|
- ${NGSPICE_LIBRARY}
|
||||||
|
+ ${EESCHEMA_KIFACE_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
if( MSVC )
|
||||||
|
@@ -649,9 +726,17 @@ set_target_properties( eeschema_kiface PROPERTIES
|
||||||
|
)
|
||||||
|
|
||||||
|
# The KIFACE is in eeschema.cpp, export it:
|
||||||
|
-set_source_files_properties( eeschema.cpp PROPERTIES
|
||||||
|
- COMPILE_DEFINITIONS "BUILD_KIWAY_DLL;COMPILING_DLL"
|
||||||
|
- )
|
||||||
|
+if( EMSCRIPTEN )
|
||||||
|
+ # WASM: Static linking - don't define BUILD_KIWAY_DLL so KIFACE_GETTER
|
||||||
|
+ # is exported with its statically-linkable name (KIFACE_1).
|
||||||
|
+ set_source_files_properties( eeschema.cpp PROPERTIES
|
||||||
|
+ COMPILE_DEFINITIONS "COMPILING_DLL"
|
||||||
|
+ )
|
||||||
|
+else()
|
||||||
|
+ set_source_files_properties( eeschema.cpp PROPERTIES
|
||||||
|
+ COMPILE_DEFINITIONS "BUILD_KIWAY_DLL;COMPILING_DLL"
|
||||||
|
+ )
|
||||||
|
+endif()
|
||||||
|
|
||||||
|
# if building eeschema, then also build eeschema_kiface if out of date.
|
||||||
|
add_dependencies( eeschema eeschema_kiface )
|
||||||
|
diff --git a/eeschema/dialogs/dialog_sim_command.cpp b/eeschema/dialogs/dialog_sim_command.cpp
|
||||||
|
index 7ec105f35a..19fa35d460 100644
|
||||||
|
--- a/eeschema/dialogs/dialog_sim_command.cpp
|
||||||
|
+++ b/eeschema/dialogs/dialog_sim_command.cpp
|
||||||
|
@@ -570,7 +570,7 @@ void DIALOG_SIM_COMMAND::updateDCSources( wxChar aType, wxChoice* aSource )
|
||||||
|
{
|
||||||
|
wxString prevSelection;
|
||||||
|
|
||||||
|
- if( !aSource->IsEmpty() && aSource->GetSelection() >= 0 )
|
||||||
|
+ if( aSource->GetCount() > 0 && aSource->GetSelection() >= 0 )
|
||||||
|
prevSelection = aSource->GetString( aSource->GetSelection() );
|
||||||
|
|
||||||
|
std::set<wxString> sourcesList;
|
||||||
|
diff --git a/eeschema/eeschema.cpp b/eeschema/eeschema.cpp
|
||||||
|
index f3b334d1c8..d56ca181dc 100644
|
||||||
|
--- a/eeschema/eeschema.cpp
|
||||||
|
+++ b/eeschema/eeschema.cpp
|
||||||
|
@@ -201,7 +201,13 @@ static struct IFACE : public KIFACE_BASE, public UNITS_PROVIDER
|
||||||
|
}
|
||||||
|
|
||||||
|
case FRAME_SCH_SYMBOL_EDITOR:
|
||||||
|
+#ifdef __EMSCRIPTEN__
|
||||||
|
+ // WASM build: symbol library editor is not supported (see
|
||||||
|
+ // features/schematic/0001-eeschema-iface-stubs.md).
|
||||||
|
+ return nullptr;
|
||||||
|
+#else
|
||||||
|
return new SYMBOL_EDIT_FRAME( aKiway, aParent );
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
case FRAME_SIMULATOR:
|
||||||
|
{
|
||||||
|
@@ -219,10 +225,19 @@ static struct IFACE : public KIFACE_BASE, public UNITS_PROVIDER
|
||||||
|
}
|
||||||
|
|
||||||
|
case FRAME_SCH_VIEWER:
|
||||||
|
+#ifdef __EMSCRIPTEN__
|
||||||
|
+ // WASM build: symbol viewer is not supported.
|
||||||
|
+ return nullptr;
|
||||||
|
+#else
|
||||||
|
return new SYMBOL_VIEWER_FRAME( aKiway, aParent );
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
case FRAME_SYMBOL_CHOOSER:
|
||||||
|
{
|
||||||
|
+#ifdef __EMSCRIPTEN__
|
||||||
|
+ // WASM build: symbol chooser is not supported (no bundled libs).
|
||||||
|
+ return nullptr;
|
||||||
|
+#else
|
||||||
|
bool cancelled = false;
|
||||||
|
SYMBOL_CHOOSER_FRAME* chooser = new SYMBOL_CHOOSER_FRAME( aKiway, aParent, cancelled );
|
||||||
|
|
||||||
|
@@ -233,6 +248,7 @@ static struct IFACE : public KIFACE_BASE, public UNITS_PROVIDER
|
||||||
|
}
|
||||||
|
|
||||||
|
return chooser;
|
||||||
|
+#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
case DIALOG_SCH_LIBRARY_TABLE:
|
||||||
|
diff --git a/eeschema/sch_base_frame.cpp b/eeschema/sch_base_frame.cpp
|
||||||
|
index 161379a699..e722b7a043 100644
|
||||||
|
--- a/eeschema/sch_base_frame.cpp
|
||||||
|
+++ b/eeschema/sch_base_frame.cpp
|
||||||
|
@@ -775,6 +775,7 @@ wxString SCH_BASE_FRAME::SelectLibrary( const wxString& aDialogTitle, const wxSt
|
||||||
|
|
||||||
|
void SCH_BASE_FRAME::setSymWatcher( const LIB_ID* aID )
|
||||||
|
{
|
||||||
|
+#if wxUSE_FSWATCHER
|
||||||
|
Unbind( wxEVT_FSWATCHER, &SCH_BASE_FRAME::OnSymChange, this );
|
||||||
|
|
||||||
|
if( m_watcher )
|
||||||
|
@@ -824,11 +825,15 @@ void SCH_BASE_FRAME::setSymWatcher( const LIB_ID* aID )
|
||||||
|
wxLogNull silence;
|
||||||
|
m_watcher->Add( fn );
|
||||||
|
}
|
||||||
|
+#else
|
||||||
|
+ (void) aID;
|
||||||
|
+#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SCH_BASE_FRAME::OnSymChange( wxFileSystemWatcherEvent& aEvent )
|
||||||
|
{
|
||||||
|
+#if wxUSE_FSWATCHER
|
||||||
|
LEGACY_SYMBOL_LIBS* libs = PROJECT_SCH::LegacySchLibs( &Prj() );
|
||||||
|
|
||||||
|
wxLogTrace( traceLibWatch, "OnSymChange: %s, watcher file: %s",
|
||||||
|
@@ -846,6 +851,9 @@ void SCH_BASE_FRAME::OnSymChange( wxFileSystemWatcherEvent& aEvent )
|
||||||
|
wxLogTrace( traceLibWatch, "Failed to start the debounce timer" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
+#else
|
||||||
|
+ (void) aEvent;
|
||||||
|
+#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
diff --git a/eeschema/sch_base_frame.h b/eeschema/sch_base_frame.h
|
||||||
|
index 2d701c7d28..a86ae37237 100644
|
||||||
|
--- a/eeschema/sch_base_frame.h
|
||||||
|
+++ b/eeschema/sch_base_frame.h
|
||||||
|
@@ -323,7 +323,9 @@ protected:
|
||||||
|
private:
|
||||||
|
|
||||||
|
/// These are file watchers for the symbol library tables.
|
||||||
|
+#if wxUSE_FSWATCHER
|
||||||
|
std::unique_ptr<wxFileSystemWatcher> m_watcher;
|
||||||
|
+#endif
|
||||||
|
wxFileName m_watcherFileName;
|
||||||
|
wxDateTime m_watcherLastModified;
|
||||||
|
wxTimer m_watcherDebounceTimer;
|
||||||
|
diff --git a/eeschema/sch_edit_frame.cpp b/eeschema/sch_edit_frame.cpp
|
||||||
|
index fd5a91a388..6eab185446 100644
|
||||||
|
--- a/eeschema/sch_edit_frame.cpp
|
||||||
|
+++ b/eeschema/sch_edit_frame.cpp
|
||||||
|
@@ -56,7 +56,9 @@
|
||||||
|
#include <core/profile.h>
|
||||||
|
#include <project/project_file.h>
|
||||||
|
#include <project/net_settings.h>
|
||||||
|
+#ifdef KICAD_SCRIPTING
|
||||||
|
#include <python_scripting.h>
|
||||||
|
+#endif
|
||||||
|
#include <sch_edit_frame.h>
|
||||||
|
#include <symbol_chooser_frame.h>
|
||||||
|
#include <sch_painter.h>
|
||||||
|
diff --git a/eeschema/sch_io/sch_io_mgr.cpp b/eeschema/sch_io/sch_io_mgr.cpp
|
||||||
|
index 63e55bcdae..922fef0d90 100644
|
||||||
|
--- a/eeschema/sch_io/sch_io_mgr.cpp
|
||||||
|
+++ b/eeschema/sch_io/sch_io_mgr.cpp
|
||||||
|
@@ -24,17 +24,22 @@
|
||||||
|
#include <wx/uri.h>
|
||||||
|
|
||||||
|
#include <sch_io/sch_io_mgr.h>
|
||||||
|
-#include <sch_io/eagle/sch_io_eagle.h>
|
||||||
|
#include <sch_io/kicad_legacy/sch_io_kicad_legacy.h>
|
||||||
|
#include <sch_io/kicad_sexpr/sch_io_kicad_sexpr.h>
|
||||||
|
+#include <sch_io/http_lib/sch_io_http_lib.h>
|
||||||
|
|
||||||
|
+// Third-party importers and the database plugin are excluded from the WASM
|
||||||
|
+// build (see kicad/eeschema/CMakeLists.txt). Their FindPlugin cases below
|
||||||
|
+// return nullptr on WASM.
|
||||||
|
+#ifndef __EMSCRIPTEN__
|
||||||
|
+#include <sch_io/eagle/sch_io_eagle.h>
|
||||||
|
#include <sch_io/altium/sch_io_altium.h>
|
||||||
|
#include <sch_io/cadstar/sch_io_cadstar_archive.h>
|
||||||
|
#include <sch_io/easyeda/sch_io_easyeda.h>
|
||||||
|
#include <sch_io/easyedapro/sch_io_easyedapro.h>
|
||||||
|
#include <sch_io/database/sch_io_database.h>
|
||||||
|
#include <sch_io/ltspice/sch_io_ltspice.h>
|
||||||
|
-#include <sch_io/http_lib/sch_io_http_lib.h>
|
||||||
|
+#endif
|
||||||
|
#include <common.h> // for ExpandEnvVarSubstitutions
|
||||||
|
|
||||||
|
#include <wildcards_and_files_ext.h>
|
||||||
|
@@ -68,6 +73,8 @@ SCH_IO* SCH_IO_MGR::FindPlugin( SCH_FILE_T aFileType )
|
||||||
|
{
|
||||||
|
case SCH_KICAD: return new SCH_IO_KICAD_SEXPR();
|
||||||
|
case SCH_LEGACY: return new SCH_IO_KICAD_LEGACY();
|
||||||
|
+ case SCH_HTTP: return new SCH_IO_HTTP_LIB();
|
||||||
|
+#ifndef __EMSCRIPTEN__
|
||||||
|
case SCH_ALTIUM: return new SCH_IO_ALTIUM();
|
||||||
|
case SCH_CADSTAR_ARCHIVE: return new SCH_IO_CADSTAR_ARCHIVE();
|
||||||
|
case SCH_DATABASE: return new SCH_IO_DATABASE();
|
||||||
|
@@ -75,7 +82,7 @@ SCH_IO* SCH_IO_MGR::FindPlugin( SCH_FILE_T aFileType )
|
||||||
|
case SCH_EASYEDA: return new SCH_IO_EASYEDA();
|
||||||
|
case SCH_EASYEDAPRO: return new SCH_IO_EASYEDAPRO();
|
||||||
|
case SCH_LTSPICE: return new SCH_IO_LTSPICE();
|
||||||
|
- case SCH_HTTP: return new SCH_IO_HTTP_LIB();
|
||||||
|
+#endif
|
||||||
|
default: return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diff --git a/eeschema/toolbars_sch_editor.cpp b/eeschema/toolbars_sch_editor.cpp
|
||||||
|
index 600f4379bc..a4df373475 100644
|
||||||
|
--- a/eeschema/toolbars_sch_editor.cpp
|
||||||
|
+++ b/eeschema/toolbars_sch_editor.cpp
|
||||||
|
@@ -32,7 +32,9 @@
|
||||||
|
#include <bitmaps.h>
|
||||||
|
#include <eeschema_id.h>
|
||||||
|
#include <pgm_base.h>
|
||||||
|
+#ifdef KICAD_SCRIPTING
|
||||||
|
#include <python_scripting.h>
|
||||||
|
+#endif
|
||||||
|
#include <tool/tool_manager.h>
|
||||||
|
#include <tool/action_toolbar.h>
|
||||||
|
#include <tools/sch_actions.h>
|
||||||
|
@@ -251,7 +253,11 @@ void SCH_EDIT_FRAME::configureToolbars()
|
||||||
|
[this]( ACTION_TOOLBAR* aToolbar )
|
||||||
|
{
|
||||||
|
// Add scripting console and API plugins
|
||||||
|
+ #ifdef KICAD_SCRIPTING
|
||||||
|
bool scriptingAvailable = SCRIPTING::IsWxAvailable();
|
||||||
|
+ #else
|
||||||
|
+ bool scriptingAvailable = false;
|
||||||
|
+ #endif
|
||||||
|
|
||||||
|
#ifdef KICAD_IPC_API
|
||||||
|
bool haveApiPlugins = Pgm().GetCommonSettings()->m_Api.enable_server &&
|
||||||
2005
features/schematic/root.patch
Normal file
2005
features/schematic/root.patch
Normal file
File diff suppressed because it is too large
Load diff
2
kicad
2
kicad
|
|
@ -1 +1 @@
|
||||||
Subproject commit 07d8130d44493fc949da3611d50e0c3b1652f930
|
Subproject commit 4cad41af0665d166f9da4c69ae1e42d5a043538f
|
||||||
|
|
@ -9,15 +9,35 @@ FEATURE_DIR="features/${BRANCH}"
|
||||||
|
|
||||||
mkdir -p "$FEATURE_DIR"
|
mkdir -p "$FEATURE_DIR"
|
||||||
|
|
||||||
# Root repo patch (exclude submodules)
|
# Root repo patch (exclude submodules and features/ — the latter would cause
|
||||||
git diff HEAD -- ':!kicad' ':!wxwidgets' > "$FEATURE_DIR/root.patch"
|
# the patch to contain itself recursively).
|
||||||
|
git diff HEAD -- ':!kicad' ':!wxwidgets' ':!features' > "$FEATURE_DIR/root.patch"
|
||||||
|
|
||||||
# Submodule patches (diff from upstream base)
|
# Submodule patches: diff against main's recorded submodule sha so the patch
|
||||||
KICAD_BASE=$(git -C kicad log --format='%H' --author-not='viktor.vaczi@emergence-engineering.com' --author-not='noreply@anthropic.com' -1)
|
# captures only this feature branch's submodule work (committed + uncommitted),
|
||||||
git -C kicad diff $KICAD_BASE > "$FEATURE_DIR/kicad.patch"
|
# never upstream changes that landed on main.
|
||||||
|
sub_diff() {
|
||||||
|
local sub="$1"
|
||||||
|
local out="$2"
|
||||||
|
local main_sha
|
||||||
|
main_sha=$(git ls-tree origin/main "$sub" 2>/dev/null | awk '{print $3}')
|
||||||
|
if [ -z "$main_sha" ]; then
|
||||||
|
echo "Warning: could not resolve origin/main:$sub — skipping $out" >&2
|
||||||
|
rm -f "$out"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local cur_sha
|
||||||
|
cur_sha=$(git -C "$sub" rev-parse HEAD)
|
||||||
|
if [ "$main_sha" = "$cur_sha" ] && git -C "$sub" diff --quiet; then
|
||||||
|
echo "No feature-specific $sub changes (submodule pointer matches main, worktree clean) — skipping $(basename "$out")"
|
||||||
|
rm -f "$out"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
git -C "$sub" diff "$main_sha" > "$out"
|
||||||
|
}
|
||||||
|
|
||||||
WX_BASE="v3.2.6"
|
sub_diff kicad "$FEATURE_DIR/kicad.patch"
|
||||||
git -C wxwidgets diff $WX_BASE > "$FEATURE_DIR/wxwidgets.patch"
|
sub_diff wxwidgets "$FEATURE_DIR/wxwidgets.patch"
|
||||||
|
|
||||||
echo "Patches created in $FEATURE_DIR/"
|
echo "Patches created in $FEATURE_DIR/"
|
||||||
ls -la "$FEATURE_DIR"/*.patch 2>/dev/null || echo "No patches generated"
|
ls -la "$FEATURE_DIR"/*.patch 2>/dev/null || echo "No patches generated"
|
||||||
|
|
|
||||||
7
scripts/kicad/build-eeschema.sh
Executable file
7
scripts/kicad/build-eeschema.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Build KiCad Eeschema (schematic editor) for WebAssembly.
|
||||||
|
# Thin wrapper around build-kicad-target.sh — see that script for options.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
exec "${SCRIPT_DIR}/build-kicad-target.sh" eeschema "$@"
|
||||||
395
scripts/kicad/build-kicad-target.sh
Executable file
395
scripts/kicad/build-kicad-target.sh
Executable file
|
|
@ -0,0 +1,395 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Build a KiCad editor (pcbnew or eeschema) for WebAssembly.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/kicad/build-kicad-target.sh <app> [options]
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# <app> pcbnew | eeschema (required)
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --full Full clean rebuild (dependencies + KiCad)
|
||||||
|
# --clean-kicad Clean only KiCad build directory (not deps)
|
||||||
|
# --build-deps Build dependencies (default: skip)
|
||||||
|
# --debug Build with debug symbols (default)
|
||||||
|
# --release Build optimized without debug symbols
|
||||||
|
# --diag=... Diagnostic preprocessor flags (gal, coroutine, ctor, all)
|
||||||
|
# -j N Parallel compilation jobs (default: 1)
|
||||||
|
#
|
||||||
|
# Each editor builds into its own tree: build-wasm/kicad-<app>/.
|
||||||
|
# Per-editor extras live alongside generic stubs:
|
||||||
|
# - wasm/bindings/<app>_embind.cpp (optional)
|
||||||
|
# - wasm/stubs/<app>_frame_stub.cpp (optional, app-specific stubs)
|
||||||
|
# - wasm/stubs/<app>_scripting_stub.cpp (optional, app-specific scripting stubs)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Error: missing <app> argument (pcbnew | eeschema)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
APP_NAME="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "$APP_NAME" in
|
||||||
|
pcbnew|eeschema) ;;
|
||||||
|
*)
|
||||||
|
echo "Error: unknown app '$APP_NAME' (expected: pcbnew | eeschema)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
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-${APP_NAME}"
|
||||||
|
KICAD_STAMP="${BUILD_ROOT}/stamps/kicad-${APP_NAME}.stamp"
|
||||||
|
WASM_LAYER="${PROJECT_ROOT}/wasm"
|
||||||
|
WX_BUILD="${BUILD_ROOT}/wxwidgets-universal"
|
||||||
|
|
||||||
|
# Parse arguments - incremental build by default (optimized for development)
|
||||||
|
NO_CLEAN=1
|
||||||
|
FULL_CLEAN=0
|
||||||
|
SKIP_DEPS=1
|
||||||
|
DEBUG=0
|
||||||
|
DIAG_LIST=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--full)
|
||||||
|
FULL_CLEAN=1
|
||||||
|
NO_CLEAN=0
|
||||||
|
SKIP_DEPS=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--clean-kicad)
|
||||||
|
NO_CLEAN=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--build-deps)
|
||||||
|
SKIP_DEPS=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--debug)
|
||||||
|
DEBUG=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--release)
|
||||||
|
DEBUG_BUILD=0
|
||||||
|
export DEBUG_BUILD
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--diag=*)
|
||||||
|
DIAG_LIST="${1#--diag=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--diag)
|
||||||
|
DIAG_LIST="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-j)
|
||||||
|
export JOBS="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-j*)
|
||||||
|
export JOBS="${1#-j}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Diagnostic preprocessor defines from --diag=<csv> (gal, coroutine, ctor, all).
|
||||||
|
# These gate the KI_DIAG_* macros in kicad/include/kicad_wasm_diag.h. Output goes
|
||||||
|
# to stdout ([KICAD_OUT] logs), never errors. Off by default.
|
||||||
|
DIAG_DEFINES=""
|
||||||
|
if [ -n "${DIAG_LIST}" ]; then
|
||||||
|
IFS=',' read -ra _diag_cats <<< "${DIAG_LIST}"
|
||||||
|
for _cat in "${_diag_cats[@]}"; do
|
||||||
|
case "${_cat}" in
|
||||||
|
gal) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_GAL=1" ;;
|
||||||
|
coroutine) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_COROUTINE=1" ;;
|
||||||
|
ctor) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_CTOR=1" ;;
|
||||||
|
all) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_GAL=1 -DKICAD_DIAG_COROUTINE=1 -DKICAD_DIAG_CTOR=1" ;;
|
||||||
|
"") ;;
|
||||||
|
*) log_warn "Unknown --diag category: '${_cat}' (valid: gal, coroutine, ctor, all)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
log_info "Diagnostic logging enabled:${DIAG_DEFINES}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Building app: ${APP_NAME}"
|
||||||
|
log_info "Using ${JOBS} parallel jobs"
|
||||||
|
|
||||||
|
# Step 1: Clean build directories
|
||||||
|
if [ $FULL_CLEAN -eq 1 ]; then
|
||||||
|
log_info "Full clean: removing all stamps and build directories..."
|
||||||
|
rm -rf "${STAMPS_DIR}"/*
|
||||||
|
rm -rf "${BUILD_ROOT}/deps"/*
|
||||||
|
rm -rf "${BUILD_ROOT}/wxwidgets-universal"
|
||||||
|
rm -rf "${BUILD_ROOT}/stubs"
|
||||||
|
rm -rf "${KICAD_BUILD}"
|
||||||
|
rm -rf "${SYSROOT}"/*
|
||||||
|
elif [ $NO_CLEAN -eq 0 ]; then
|
||||||
|
log_info "Cleaning KiCad ${APP_NAME} build directory..."
|
||||||
|
rm -rf "${KICAD_BUILD}" "${KICAD_STAMP}"
|
||||||
|
else
|
||||||
|
log_info "Incremental build (use --clean-kicad or --full to clean)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 2: Build dependencies
|
||||||
|
# Note: --with-occ for OpenCASCADE, but NOT ngspice since KICAD_SPICE=OFF
|
||||||
|
if [ $SKIP_DEPS -eq 0 ]; then
|
||||||
|
log_info "Building dependencies..."
|
||||||
|
"${SCRIPT_DIR}/../deps/build-all-deps.sh" --with-occ
|
||||||
|
else
|
||||||
|
log_info "Skipping dependencies (use --build-deps or --full to build)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Note: We don't check the KiCad stamp here for incremental builds.
|
||||||
|
# CMake handles dependency tracking - it will detect changed source files
|
||||||
|
# and only recompile what's needed. The stamp is created at the end for
|
||||||
|
# scripts that want to know if KiCad was ever built successfully.
|
||||||
|
|
||||||
|
# Step 4: Build wxWidgets (incremental - only recompiles changed files)
|
||||||
|
log_info "Building wxWidgets..."
|
||||||
|
"${SCRIPT_DIR}/../build-wxuniversal-wasm.sh" --no-clean
|
||||||
|
|
||||||
|
log_info "Building KiCad ${APP_NAME} ${KICAD_VERSION} for WASM..."
|
||||||
|
|
||||||
|
# Step 5: Set build type
|
||||||
|
# Use environment DEBUG_BUILD if set, otherwise check local --debug flag
|
||||||
|
# -fexceptions is required because wxWidgets is built with exceptions enabled
|
||||||
|
# -matomics -mbulk-memory are required for shared memory (pthreads)
|
||||||
|
# NOTE: We use -O1 for debug builds because -O0 produces WASM with too many
|
||||||
|
# locals for V8/Chrome to compile (error: "local count too large").
|
||||||
|
# -O1 keeps debug info but optimizes enough to stay under V8's limits.
|
||||||
|
if [ "${DEBUG_BUILD:-0}" = "1" ] || [ $DEBUG -eq 1 ]; then
|
||||||
|
BUILD_TYPE="Debug"
|
||||||
|
EXTRA_FLAGS="-g -O1 -fexceptions -matomics -mbulk-memory"
|
||||||
|
# -gseparate-dwarf puts debug info in a separate .debug.wasm file
|
||||||
|
# This keeps the main WASM small (~200MB) while preserving full debug info
|
||||||
|
# DevTools loads the debug file on-demand when debugging
|
||||||
|
LINKER_DEBUG_FLAGS="-O1 -g -gseparate-dwarf -fexceptions"
|
||||||
|
log_info "Building KiCad in DEBUG mode (separate DWARF for smaller main binary)"
|
||||||
|
else
|
||||||
|
BUILD_TYPE="Release"
|
||||||
|
EXTRA_FLAGS="-O2 -fexceptions -matomics -mbulk-memory"
|
||||||
|
# -O0 at link time skips wasm-opt (which can OOM on large WASM files)
|
||||||
|
# Compilation is still -O2 for optimized code, but we skip post-link wasm-opt
|
||||||
|
LINKER_DEBUG_FLAGS="-O0 -fexceptions"
|
||||||
|
log_info "Building KiCad in RELEASE mode (skipping wasm-opt due to memory limits)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 6: Create build directory
|
||||||
|
mkdir -p "${KICAD_BUILD}"
|
||||||
|
cd "${KICAD_BUILD}"
|
||||||
|
|
||||||
|
# Step 6.1: Build stub libraries for missing symbols
|
||||||
|
# Generic stubs (libgit2, curl, nng) are shared across apps and built in BUILD_ROOT/stubs.
|
||||||
|
# App-specific stubs (e.g. pcbnew_scripting_stub) build into the same directory but
|
||||||
|
# are only linked in when the corresponding source exists.
|
||||||
|
STUBS_DIR="${PROJECT_ROOT}/wasm/stubs"
|
||||||
|
STUBS_BUILD="${BUILD_ROOT}/stubs"
|
||||||
|
mkdir -p "${STUBS_BUILD}"
|
||||||
|
|
||||||
|
log_info "Building stub libraries..."
|
||||||
|
# Compile libgit2 stub
|
||||||
|
emcc -c "${STUBS_DIR}/libgit2_stub.c" -o "${STUBS_BUILD}/libgit2_stub.o"
|
||||||
|
emar rcs "${STUBS_BUILD}/libgit2_stub.a" "${STUBS_BUILD}/libgit2_stub.o"
|
||||||
|
|
||||||
|
# Compile curl stub
|
||||||
|
emcc -c "${STUBS_DIR}/curl_stub.c" -o "${STUBS_BUILD}/curl_stub.o"
|
||||||
|
emar rcs "${STUBS_BUILD}/libcurl_stub.a" "${STUBS_BUILD}/curl_stub.o"
|
||||||
|
|
||||||
|
# Note: GLU tesselator is now implemented in wasm/stubs/glu_wasm_impl.cpp
|
||||||
|
# It's compiled as part of the GAL library (requires KiCad headers)
|
||||||
|
|
||||||
|
# Compile NNG stub (IPC API requires NNG but sockets don't work in WASM)
|
||||||
|
emcc -c -I"${STUBS_DIR}" "${STUBS_DIR}/nng_stub.c" -o "${STUBS_BUILD}/nng_stub.o"
|
||||||
|
emar rcs "${STUBS_BUILD}/libnng_stub.a" "${STUBS_BUILD}/nng_stub.o"
|
||||||
|
|
||||||
|
# wx flags for any C++ stubs that include wx headers
|
||||||
|
WX_CXXFLAGS=$("${WX_BUILD}/wx-config" --cxxflags 2>/dev/null || echo "-I${WX_BUILD}/lib/wx/include/emscripten-unicode-static-3.2 -I${PROJECT_ROOT}/wxwidgets/include")
|
||||||
|
|
||||||
|
# App-specific stubs:
|
||||||
|
# - pcbnew: pcbnew_scripting_stub.cpp (action-plugin scripting placeholders)
|
||||||
|
# - eeschema: eeschema_frame_stub.cpp (placeholder; grows as linker dictates)
|
||||||
|
APP_STUB_LINK=""
|
||||||
|
APP_SCRIPTING_STUB_SRC="${STUBS_DIR}/${APP_NAME}_scripting_stub.cpp"
|
||||||
|
if [ -f "${APP_SCRIPTING_STUB_SRC}" ]; then
|
||||||
|
log_info "Building app scripting stub: ${APP_NAME}_scripting_stub.cpp"
|
||||||
|
em++ -c ${WX_CXXFLAGS} "${APP_SCRIPTING_STUB_SRC}" -o "${STUBS_BUILD}/${APP_NAME}_scripting_stub.o"
|
||||||
|
emar rcs "${STUBS_BUILD}/lib${APP_NAME}_scripting_stub.a" "${STUBS_BUILD}/${APP_NAME}_scripting_stub.o"
|
||||||
|
APP_STUB_LINK="${APP_STUB_LINK} ${STUBS_BUILD}/lib${APP_NAME}_scripting_stub.a"
|
||||||
|
fi
|
||||||
|
|
||||||
|
APP_FRAME_STUB_SRC="${STUBS_DIR}/${APP_NAME}_frame_stub.cpp"
|
||||||
|
if [ -f "${APP_FRAME_STUB_SRC}" ] && [ -s "${APP_FRAME_STUB_SRC}" ]; then
|
||||||
|
log_info "Building app frame stub: ${APP_NAME}_frame_stub.cpp"
|
||||||
|
em++ -c ${WX_CXXFLAGS} "${APP_FRAME_STUB_SRC}" -o "${STUBS_BUILD}/${APP_NAME}_frame_stub.o"
|
||||||
|
emar rcs "${STUBS_BUILD}/lib${APP_NAME}_frame_stub.a" "${STUBS_BUILD}/${APP_NAME}_frame_stub.o"
|
||||||
|
APP_STUB_LINK="${APP_STUB_LINK} ${STUBS_BUILD}/lib${APP_NAME}_frame_stub.a"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Stub libraries built"
|
||||||
|
|
||||||
|
# Step 6.2: Replace Emscripten's wasm-opt with stub to bypass asyncify transformation
|
||||||
|
# This allows Emscripten to generate JS with Asyncify runtime, but we run the real
|
||||||
|
# wasm-opt --asyncify on the host where more RAM is available (needs 50GB+ for KiCad)
|
||||||
|
if [ -z "${EMSDK}" ]; then
|
||||||
|
log_error "EMSDK environment variable is not set."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
EMSDK_WASM_OPT="${EMSDK}/upstream/bin/wasm-opt"
|
||||||
|
if [ -f "${EMSDK_WASM_OPT}" ] && [ ! -f "${EMSDK_WASM_OPT}.real" ]; then
|
||||||
|
log_info "Backing up real wasm-opt..."
|
||||||
|
mv "${EMSDK_WASM_OPT}" "${EMSDK_WASM_OPT}.real"
|
||||||
|
fi
|
||||||
|
# Always copy the latest stub (in case it was updated)
|
||||||
|
cp "${STUBS_DIR}/wasm-opt-stub.sh" "${EMSDK_WASM_OPT}"
|
||||||
|
chmod +x "${EMSDK_WASM_OPT}"
|
||||||
|
log_info "wasm-opt stub installed (asyncify will run on host)"
|
||||||
|
|
||||||
|
# Step 6.3: Replace wasm-emscripten-finalize with stub (same pattern as wasm-opt)
|
||||||
|
# This tool also OOMs on large WASM with debug symbols, so we run it on the host
|
||||||
|
EMSDK_FINALIZE="${EMSDK}/upstream/bin/wasm-emscripten-finalize"
|
||||||
|
if [ -f "${EMSDK_FINALIZE}" ] && [ ! -f "${EMSDK_FINALIZE}.real" ]; then
|
||||||
|
log_info "Backing up real wasm-emscripten-finalize..."
|
||||||
|
mv "${EMSDK_FINALIZE}" "${EMSDK_FINALIZE}.real"
|
||||||
|
fi
|
||||||
|
# Always copy the latest stub (in case it was updated)
|
||||||
|
cp "${STUBS_DIR}/wasm-emscripten-finalize-stub.sh" "${EMSDK_FINALIZE}"
|
||||||
|
chmod +x "${EMSDK_FINALIZE}"
|
||||||
|
log_info "wasm-emscripten-finalize stub installed (finalize will run on host)"
|
||||||
|
|
||||||
|
# Step 6.5: Verify WASM support is in KiCad fork
|
||||||
|
# The kicad submodule should already have WASM port detection and kiplatform support
|
||||||
|
KICAD_CMAKE="${KICAD_DIR}/CMakeLists.txt"
|
||||||
|
if ! grep -q "msw|qt|gtk|osx|wasm" "${KICAD_CMAKE}"; then
|
||||||
|
log_error "KiCad fork is missing WASM port detection support."
|
||||||
|
log_error "Please ensure the kicad submodule has WASM modifications."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
KIPLATFORM_CMAKE="${KICAD_DIR}/libs/kiplatform/CMakeLists.txt"
|
||||||
|
if ! grep -q "KICAD_WX_PORT STREQUAL wasm" "${KIPLATFORM_CMAKE}"; then
|
||||||
|
log_error "KiCad fork is missing kiplatform WASM support."
|
||||||
|
log_error "Please ensure the kicad submodule has WASM modifications."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_info "KiCad WASM support verified"
|
||||||
|
|
||||||
|
# Embind object — built after CMake configure runs (so config.h exists). The
|
||||||
|
# linker line below references "${STUBS_BUILD}/${APP_NAME}_embind.o" so we
|
||||||
|
# create an empty placeholder when the source is missing, to keep the link
|
||||||
|
# line stable across apps.
|
||||||
|
EMBIND_OBJ="${STUBS_BUILD}/${APP_NAME}_embind.o"
|
||||||
|
EMBIND_SRC="${PROJECT_ROOT}/wasm/bindings/${APP_NAME}_embind.cpp"
|
||||||
|
|
||||||
|
# Step 7: Configure KiCad with CMake
|
||||||
|
# We use CMAKE_MODULE_PATH to inject our compatibility layer
|
||||||
|
log_info "Configuring KiCad with CMake..."
|
||||||
|
|
||||||
|
# Use ccache if available (CMAKE_*_COMPILER_LAUNCHER is the proper CMake way)
|
||||||
|
CCACHE_OPTS=""
|
||||||
|
if command -v ccache &> /dev/null; then
|
||||||
|
CCACHE_OPTS="-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache"
|
||||||
|
log_info "Using ccache for compilation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
emcmake cmake "${KICAD_DIR}" \
|
||||||
|
${CCACHE_OPTS} \
|
||||||
|
-DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
|
||||||
|
-DCMAKE_INSTALL_PREFIX="${SYSROOT}" \
|
||||||
|
-DCMAKE_MODULE_PATH="${WASM_LAYER}/cmake" \
|
||||||
|
-DSYSROOT="${SYSROOT}" \
|
||||||
|
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
||||||
|
-DCMAKE_CXX_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -DKICAD_USE_PLATFORM_WASM=1${DIAG_DEFINES} -I${SYSROOT}/include -I${STUBS_DIR} -include ${STUBS_DIR}/char_traits_uint16_workaround.h" \
|
||||||
|
-DCMAKE_C_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -I${SYSROOT}/include -I${STUBS_DIR}" \
|
||||||
|
-DCMAKE_EXE_LINKER_FLAGS="${LINKER_DEBUG_FLAGS} -pthread -sUSE_ZLIB=1 -sASYNCIFY=1 -sDYNCALLS=1 -sASYNCIFY_STACK_SIZE=65536 -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' -sPTHREAD_POOL_SIZE_STRICT=0 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB -sMAX_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','dynCall'] -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['\$dynCall'] --bind -L${SYSROOT}/lib ${STUBS_BUILD}/libgit2_stub.a ${STUBS_BUILD}/libcurl_stub.a${APP_STUB_LINK} ${STUBS_BUILD}/libnng_stub.a ${EMBIND_OBJ}" \
|
||||||
|
-DCMAKE_PREFIX_PATH="${SYSROOT};${WX_BUILD}" \
|
||||||
|
-DwxWidgets_CONFIG_EXECUTABLE="${WX_BUILD}/wx-config" \
|
||||||
|
\
|
||||||
|
-DKICAD_BUILD_QA_TESTS=OFF \
|
||||||
|
-DKICAD_SPICE=OFF \
|
||||||
|
-DKICAD_USE_EGL=OFF \
|
||||||
|
-DKICAD_USE_BUNDLED_GLEW=ON \
|
||||||
|
-DKICAD_BUILD_3D_VIEWER_WASM=OFF \
|
||||||
|
-DKICAD_IPC_API=ON \
|
||||||
|
-DKICAD_USE_PCH=ON \
|
||||||
|
\
|
||||||
|
-DZSTD_ROOT="${SYSROOT}" \
|
||||||
|
-DZSTD_INCLUDE_DIR="${SYSROOT}/include" \
|
||||||
|
-DZSTD_LIBRARY="${SYSROOT}/lib/libzstd.a" \
|
||||||
|
-DGLM_INCLUDE_DIR="${SYSROOT}/include" \
|
||||||
|
-DGLM_VERSION="0.9.9.8" \
|
||||||
|
-DBOOST_ROOT="${SYSROOT}" \
|
||||||
|
-DBoost_INCLUDE_DIR="${SYSROOT}/include" \
|
||||||
|
-DBoost_LIBRARY_DIR="${SYSROOT}/lib" \
|
||||||
|
-DBoost_NO_SYSTEM_PATHS=ON \
|
||||||
|
-DBoost_NO_BOOST_CMAKE=ON \
|
||||||
|
-DFREETYPE_INCLUDE_DIR_ft2build="${SYSROOT}/include/freetype2" \
|
||||||
|
-DFREETYPE_INCLUDE_DIR_freetype2="${SYSROOT}/include/freetype2" \
|
||||||
|
-DFREETYPE_LIBRARY="${SYSROOT}/lib/libfreetype.a" \
|
||||||
|
-DHarfBuzz_INCLUDE_DIR="${SYSROOT}/include/harfbuzz" \
|
||||||
|
-DHarfBuzz_LIBRARY="${SYSROOT}/lib/libharfbuzz.a" \
|
||||||
|
-DOCC_INCLUDE_DIR="${SYSROOT}/include/opencascade" \
|
||||||
|
-DOCC_LIBRARY_DIR="${SYSROOT}/lib" \
|
||||||
|
-DProtobuf_INCLUDE_DIR="${SYSROOT}/include" \
|
||||||
|
-DProtobuf_LIBRARY="${SYSROOT}/lib/libprotobuf.a" \
|
||||||
|
-DProtobuf_LITE_LIBRARY="${SYSROOT}/lib/libprotobuf-lite.a" \
|
||||||
|
-DProtobuf_PROTOC_EXECUTABLE="${SYSROOT}/bin/protoc" \
|
||||||
|
-DODBC_CONFIG:STRING="stub-for-wasm" \
|
||||||
|
-DODBCLIB:STRING="" \
|
||||||
|
-DODBC_CFLAGS:STRING="" \
|
||||||
|
-DODBC_LINK_FLAGS:STRING="" \
|
||||||
|
-DODBC_LIBRARIES:STRING="" \
|
||||||
|
\
|
||||||
|
-DBUILD_GITHUB_PLUGIN=OFF \
|
||||||
|
-DKICAD_PCM=OFF \
|
||||||
|
\
|
||||||
|
-DHAVE_STRCASECMP=1 \
|
||||||
|
-DHAVE_STRNCASECMP=1
|
||||||
|
|
||||||
|
# Step 7.1: Compile Embind bindings (after CMake so config.h exists)
|
||||||
|
# Exposes KiCad objects to JavaScript for future Pyodide integration.
|
||||||
|
# When no app-specific source exists, build an empty object so the linker line
|
||||||
|
# referencing ${APP_NAME}_embind.o doesn't break.
|
||||||
|
if [ -f "${EMBIND_SRC}" ]; then
|
||||||
|
log_info "Compiling Embind bindings (${APP_NAME})..."
|
||||||
|
# Use the same includes and flags that KiCad uses
|
||||||
|
KICAD_INCLUDES="-I${KICAD_BUILD} -I${KICAD_DIR}/include -I${KICAD_DIR}/${APP_NAME} -I${KICAD_DIR}/common"
|
||||||
|
KICAD_INCLUDES+=" -I${KICAD_DIR}/libs/core/include -I${KICAD_DIR}/libs/kimath/include -I${KICAD_DIR}/libs/kiplatform/include"
|
||||||
|
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/clipper2/Clipper2Lib/include"
|
||||||
|
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/nlohmann_json"
|
||||||
|
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/dynamic_bitset"
|
||||||
|
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/nanodbc"
|
||||||
|
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/picosha2"
|
||||||
|
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty"
|
||||||
|
KICAD_INCLUDES+=" -I${SYSROOT}/include"
|
||||||
|
# KiCad requires C++20 for concepts
|
||||||
|
em++ -std=c++20 -c ${EXTRA_FLAGS} ${WX_CXXFLAGS} ${KICAD_INCLUDES} "${EMBIND_SRC}" -o "${EMBIND_OBJ}"
|
||||||
|
else
|
||||||
|
log_info "No embind source for ${APP_NAME} (expected at ${EMBIND_SRC}); using empty placeholder"
|
||||||
|
EMPTY_C="${STUBS_BUILD}/${APP_NAME}_embind_empty.c"
|
||||||
|
: > "${EMPTY_C}"
|
||||||
|
emcc -c "${EMPTY_C}" -o "${EMBIND_OBJ}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 8: Build the app target
|
||||||
|
log_info "Building ${APP_NAME}..."
|
||||||
|
emmake make -j${JOBS} "${APP_NAME}"
|
||||||
|
|
||||||
|
# Step 8.1: Build bitmap resources (images.tar.gz)
|
||||||
|
# This creates the icon archive that KiCad loads at runtime
|
||||||
|
log_info "Building bitmap resources..."
|
||||||
|
emmake make bitmap_archive_build
|
||||||
|
|
||||||
|
# Step 9: Create stamp file
|
||||||
|
create_stamp "${KICAD_STAMP}"
|
||||||
|
log_info "KiCad ${APP_NAME} build complete!"
|
||||||
|
log_info "Output: ${KICAD_BUILD}/${APP_NAME}/${APP_NAME}.js"
|
||||||
|
|
@ -1,349 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Build KiCad PCBnew for WebAssembly
|
# Build KiCad PCBnew for WebAssembly.
|
||||||
# This builds the PCB editor as a standalone WASM application
|
# Thin wrapper around build-kicad-target.sh — see that script for options.
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./scripts/kicad/build-pcbnew.sh [options]
|
|
||||||
#
|
|
||||||
# Options:
|
|
||||||
# --full Full clean rebuild (dependencies + KiCad)
|
|
||||||
# --clean-kicad Clean only KiCad build directory (not deps)
|
|
||||||
# --build-deps Build dependencies (default: skip)
|
|
||||||
# --debug Build with debug symbols (default)
|
|
||||||
# --release Build optimized without debug symbols
|
|
||||||
# -j N Parallel compilation jobs (default: 1)
|
|
||||||
#
|
|
||||||
# Defaults (optimized for development):
|
|
||||||
# - Incremental build (no clean)
|
|
||||||
# - Skip dependencies
|
|
||||||
# - ccache enabled for faster rebuilds
|
|
||||||
#
|
|
||||||
# Incremental Build System:
|
|
||||||
# - wxWidgets: configure runs once, make handles file-level dependencies
|
|
||||||
# - KiCad: CMake tracks dependencies, only recompiles changed files
|
|
||||||
# - ccache: Caches compiled objects for faster rebuilds
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
source "${SCRIPT_DIR}/../common/env.sh"
|
exec "${SCRIPT_DIR}/build-kicad-target.sh" pcbnew "$@"
|
||||||
source "${SCRIPT_DIR}/../common/versions.sh"
|
|
||||||
source "${SCRIPT_DIR}/../common/functions.sh"
|
|
||||||
|
|
||||||
KICAD_DIR="${PROJECT_ROOT}/kicad"
|
|
||||||
KICAD_BUILD="${BUILD_ROOT}/kicad-pcbnew"
|
|
||||||
KICAD_STAMP="${BUILD_ROOT}/stamps/kicad-pcbnew.stamp"
|
|
||||||
WASM_LAYER="${PROJECT_ROOT}/wasm"
|
|
||||||
WX_BUILD="${BUILD_ROOT}/wxwidgets-universal"
|
|
||||||
|
|
||||||
# Parse arguments - incremental build by default (optimized for development)
|
|
||||||
NO_CLEAN=1
|
|
||||||
FULL_CLEAN=0
|
|
||||||
SKIP_DEPS=1
|
|
||||||
DEBUG=0
|
|
||||||
DIAG_LIST=""
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--full)
|
|
||||||
FULL_CLEAN=1
|
|
||||||
NO_CLEAN=0
|
|
||||||
SKIP_DEPS=0
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--clean-kicad)
|
|
||||||
NO_CLEAN=0
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--build-deps)
|
|
||||||
SKIP_DEPS=0
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--debug)
|
|
||||||
DEBUG=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--release)
|
|
||||||
DEBUG_BUILD=0
|
|
||||||
export DEBUG_BUILD
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--diag=*)
|
|
||||||
DIAG_LIST="${1#--diag=}"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--diag)
|
|
||||||
DIAG_LIST="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-j)
|
|
||||||
export JOBS="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-j*)
|
|
||||||
export JOBS="${1#-j}"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Diagnostic preprocessor defines from --diag=<csv> (gal, coroutine, ctor, all).
|
|
||||||
# These gate the KI_DIAG_* macros in kicad/include/kicad_wasm_diag.h. Output goes
|
|
||||||
# to stdout ([KICAD_OUT] logs), never errors. Off by default.
|
|
||||||
DIAG_DEFINES=""
|
|
||||||
if [ -n "${DIAG_LIST}" ]; then
|
|
||||||
IFS=',' read -ra _diag_cats <<< "${DIAG_LIST}"
|
|
||||||
for _cat in "${_diag_cats[@]}"; do
|
|
||||||
case "${_cat}" in
|
|
||||||
gal) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_GAL=1" ;;
|
|
||||||
coroutine) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_COROUTINE=1" ;;
|
|
||||||
ctor) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_CTOR=1" ;;
|
|
||||||
all) DIAG_DEFINES="${DIAG_DEFINES} -DKICAD_DIAG_GAL=1 -DKICAD_DIAG_COROUTINE=1 -DKICAD_DIAG_CTOR=1" ;;
|
|
||||||
"") ;;
|
|
||||||
*) log_warn "Unknown --diag category: '${_cat}' (valid: gal, coroutine, ctor, all)" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
log_info "Diagnostic logging enabled:${DIAG_DEFINES}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Using ${JOBS} parallel jobs"
|
|
||||||
|
|
||||||
# Step 1: Clean build directories
|
|
||||||
if [ $FULL_CLEAN -eq 1 ]; then
|
|
||||||
log_info "Full clean: removing all stamps and build directories..."
|
|
||||||
rm -rf "${STAMPS_DIR}"/*
|
|
||||||
rm -rf "${BUILD_ROOT}/deps"/*
|
|
||||||
rm -rf "${BUILD_ROOT}/wxwidgets-universal"
|
|
||||||
rm -rf "${BUILD_ROOT}/stubs"
|
|
||||||
rm -rf "${KICAD_BUILD}"
|
|
||||||
rm -rf "${SYSROOT}"/*
|
|
||||||
elif [ $NO_CLEAN -eq 0 ]; then
|
|
||||||
log_info "Cleaning KiCad PCBnew build directory..."
|
|
||||||
rm -rf "${KICAD_BUILD}" "${KICAD_STAMP}"
|
|
||||||
else
|
|
||||||
log_info "Incremental build (use --clean-kicad or --full to clean)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 2: Build dependencies
|
|
||||||
# Note: --with-occ for OpenCASCADE, but NOT ngspice since KICAD_SPICE=OFF
|
|
||||||
if [ $SKIP_DEPS -eq 0 ]; then
|
|
||||||
log_info "Building dependencies..."
|
|
||||||
"${SCRIPT_DIR}/../deps/build-all-deps.sh" --with-occ
|
|
||||||
else
|
|
||||||
log_info "Skipping dependencies (use --build-deps or --full to build)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Note: We don't check the KiCad stamp here for incremental builds.
|
|
||||||
# CMake handles dependency tracking - it will detect changed source files
|
|
||||||
# and only recompile what's needed. The stamp is created at the end for
|
|
||||||
# scripts that want to know if KiCad was ever built successfully.
|
|
||||||
|
|
||||||
# Step 4: Build wxWidgets (incremental - only recompiles changed files)
|
|
||||||
# The wxWidgets build script handles:
|
|
||||||
# - Skipping configure if already configured
|
|
||||||
# - make handles per-file dependency tracking
|
|
||||||
# - ccache handles compilation caching
|
|
||||||
log_info "Building wxWidgets..."
|
|
||||||
"${SCRIPT_DIR}/../build-wxuniversal-wasm.sh" --no-clean
|
|
||||||
|
|
||||||
log_info "Building KiCad PCBnew ${KICAD_VERSION} for WASM..."
|
|
||||||
|
|
||||||
# Step 5: Set build type
|
|
||||||
# Use environment DEBUG_BUILD if set, otherwise check local --debug flag
|
|
||||||
# -fexceptions is required because wxWidgets is built with exceptions enabled
|
|
||||||
# -matomics -mbulk-memory are required for shared memory (pthreads)
|
|
||||||
# NOTE: We use -O1 for debug builds because -O0 produces WASM with too many
|
|
||||||
# locals for V8/Chrome to compile (error: "local count too large").
|
|
||||||
# -O1 keeps debug info but optimizes enough to stay under V8's limits.
|
|
||||||
if [ "${DEBUG_BUILD:-0}" = "1" ] || [ $DEBUG -eq 1 ]; then
|
|
||||||
BUILD_TYPE="Debug"
|
|
||||||
EXTRA_FLAGS="-g -O1 -fexceptions -matomics -mbulk-memory"
|
|
||||||
# -gseparate-dwarf puts debug info in a separate .debug.wasm file
|
|
||||||
# This keeps the main WASM small (~200MB) while preserving full debug info
|
|
||||||
# DevTools loads the debug file on-demand when debugging
|
|
||||||
LINKER_DEBUG_FLAGS="-O1 -g -gseparate-dwarf -fexceptions"
|
|
||||||
log_info "Building KiCad in DEBUG mode (separate DWARF for smaller main binary)"
|
|
||||||
else
|
|
||||||
BUILD_TYPE="Release"
|
|
||||||
EXTRA_FLAGS="-O2 -fexceptions -matomics -mbulk-memory"
|
|
||||||
# -O0 at link time skips wasm-opt (which can OOM on large WASM files)
|
|
||||||
# Compilation is still -O2 for optimized code, but we skip post-link wasm-opt
|
|
||||||
LINKER_DEBUG_FLAGS="-O0 -fexceptions"
|
|
||||||
log_info "Building KiCad in RELEASE mode (skipping wasm-opt due to memory limits)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 6: Create build directory
|
|
||||||
mkdir -p "${KICAD_BUILD}"
|
|
||||||
cd "${KICAD_BUILD}"
|
|
||||||
|
|
||||||
# Step 6.1: Build stub libraries for missing symbols
|
|
||||||
STUBS_DIR="${PROJECT_ROOT}/wasm/stubs"
|
|
||||||
STUBS_BUILD="${BUILD_ROOT}/stubs"
|
|
||||||
mkdir -p "${STUBS_BUILD}"
|
|
||||||
|
|
||||||
log_info "Building stub libraries..."
|
|
||||||
# Compile libgit2 stub
|
|
||||||
emcc -c "${STUBS_DIR}/libgit2_stub.c" -o "${STUBS_BUILD}/libgit2_stub.o"
|
|
||||||
emar rcs "${STUBS_BUILD}/libgit2_stub.a" "${STUBS_BUILD}/libgit2_stub.o"
|
|
||||||
|
|
||||||
# Compile curl stub
|
|
||||||
emcc -c "${STUBS_DIR}/curl_stub.c" -o "${STUBS_BUILD}/curl_stub.o"
|
|
||||||
emar rcs "${STUBS_BUILD}/libcurl_stub.a" "${STUBS_BUILD}/curl_stub.o"
|
|
||||||
|
|
||||||
# Note: GLU tesselator is now implemented in wasm/stubs/glu_wasm_impl.cpp
|
|
||||||
# It's compiled as part of the GAL library (requires KiCad headers)
|
|
||||||
|
|
||||||
# Compile PCBnew scripting stub (requires wxWidgets headers)
|
|
||||||
WX_CXXFLAGS=$("${WX_BUILD}/wx-config" --cxxflags 2>/dev/null || echo "-I${WX_BUILD}/lib/wx/include/emscripten-unicode-static-3.2 -I${PROJECT_ROOT}/wxwidgets/include")
|
|
||||||
em++ -c ${WX_CXXFLAGS} "${STUBS_DIR}/pcbnew_scripting_stub.cpp" -o "${STUBS_BUILD}/pcbnew_scripting_stub.o"
|
|
||||||
emar rcs "${STUBS_BUILD}/libpcbnew_scripting_stub.a" "${STUBS_BUILD}/pcbnew_scripting_stub.o"
|
|
||||||
|
|
||||||
# Compile NNG stub (IPC API requires NNG but sockets don't work in WASM)
|
|
||||||
emcc -c -I"${STUBS_DIR}" "${STUBS_DIR}/nng_stub.c" -o "${STUBS_BUILD}/nng_stub.o"
|
|
||||||
emar rcs "${STUBS_BUILD}/libnng_stub.a" "${STUBS_BUILD}/nng_stub.o"
|
|
||||||
|
|
||||||
log_info "Stub libraries built"
|
|
||||||
|
|
||||||
# Step 6.2: Replace Emscripten's wasm-opt with stub to bypass asyncify transformation
|
|
||||||
# This allows Emscripten to generate JS with Asyncify runtime, but we run the real
|
|
||||||
# wasm-opt --asyncify on the host where more RAM is available (needs 50GB+ for KiCad)
|
|
||||||
if [ -z "${EMSDK}" ]; then
|
|
||||||
log_error "EMSDK environment variable is not set."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
EMSDK_WASM_OPT="${EMSDK}/upstream/bin/wasm-opt"
|
|
||||||
if [ -f "${EMSDK_WASM_OPT}" ] && [ ! -f "${EMSDK_WASM_OPT}.real" ]; then
|
|
||||||
log_info "Backing up real wasm-opt..."
|
|
||||||
mv "${EMSDK_WASM_OPT}" "${EMSDK_WASM_OPT}.real"
|
|
||||||
fi
|
|
||||||
# Always copy the latest stub (in case it was updated)
|
|
||||||
cp "${STUBS_DIR}/wasm-opt-stub.sh" "${EMSDK_WASM_OPT}"
|
|
||||||
chmod +x "${EMSDK_WASM_OPT}"
|
|
||||||
log_info "wasm-opt stub installed (asyncify will run on host)"
|
|
||||||
|
|
||||||
# Step 6.3: Replace wasm-emscripten-finalize with stub (same pattern as wasm-opt)
|
|
||||||
# This tool also OOMs on large WASM with debug symbols, so we run it on the host
|
|
||||||
EMSDK_FINALIZE="${EMSDK}/upstream/bin/wasm-emscripten-finalize"
|
|
||||||
if [ -f "${EMSDK_FINALIZE}" ] && [ ! -f "${EMSDK_FINALIZE}.real" ]; then
|
|
||||||
log_info "Backing up real wasm-emscripten-finalize..."
|
|
||||||
mv "${EMSDK_FINALIZE}" "${EMSDK_FINALIZE}.real"
|
|
||||||
fi
|
|
||||||
# Always copy the latest stub (in case it was updated)
|
|
||||||
cp "${STUBS_DIR}/wasm-emscripten-finalize-stub.sh" "${EMSDK_FINALIZE}"
|
|
||||||
chmod +x "${EMSDK_FINALIZE}"
|
|
||||||
log_info "wasm-emscripten-finalize stub installed (finalize will run on host)"
|
|
||||||
|
|
||||||
# Step 6.5: Verify WASM support is in KiCad fork
|
|
||||||
# The kicad submodule should already have WASM port detection and kiplatform support
|
|
||||||
KICAD_CMAKE="${KICAD_DIR}/CMakeLists.txt"
|
|
||||||
if ! grep -q "msw|qt|gtk|osx|wasm" "${KICAD_CMAKE}"; then
|
|
||||||
log_error "KiCad fork is missing WASM port detection support."
|
|
||||||
log_error "Please ensure the kicad submodule has WASM modifications."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
KIPLATFORM_CMAKE="${KICAD_DIR}/libs/kiplatform/CMakeLists.txt"
|
|
||||||
if ! grep -q "KICAD_WX_PORT STREQUAL wasm" "${KIPLATFORM_CMAKE}"; then
|
|
||||||
log_error "KiCad fork is missing kiplatform WASM support."
|
|
||||||
log_error "Please ensure the kicad submodule has WASM modifications."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
log_info "KiCad WASM support verified"
|
|
||||||
|
|
||||||
# Step 7: Configure KiCad with CMake
|
|
||||||
# We use CMAKE_MODULE_PATH to inject our compatibility layer
|
|
||||||
log_info "Configuring KiCad with CMake..."
|
|
||||||
|
|
||||||
# Use ccache if available (CMAKE_*_COMPILER_LAUNCHER is the proper CMake way)
|
|
||||||
CCACHE_OPTS=""
|
|
||||||
if command -v ccache &> /dev/null; then
|
|
||||||
CCACHE_OPTS="-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache"
|
|
||||||
log_info "Using ccache for compilation"
|
|
||||||
fi
|
|
||||||
|
|
||||||
emcmake cmake "${KICAD_DIR}" \
|
|
||||||
${CCACHE_OPTS} \
|
|
||||||
-DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
|
|
||||||
-DCMAKE_INSTALL_PREFIX="${SYSROOT}" \
|
|
||||||
-DCMAKE_MODULE_PATH="${WASM_LAYER}/cmake" \
|
|
||||||
-DSYSROOT="${SYSROOT}" \
|
|
||||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
|
||||||
-DCMAKE_CXX_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -DKICAD_USE_PLATFORM_WASM=1${DIAG_DEFINES} -I${SYSROOT}/include -I${STUBS_DIR}" \
|
|
||||||
-DCMAKE_C_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -I${SYSROOT}/include -I${STUBS_DIR}" \
|
|
||||||
-DCMAKE_EXE_LINKER_FLAGS="${LINKER_DEBUG_FLAGS} -pthread -sUSE_ZLIB=1 -sASYNCIFY=1 -sDYNCALLS=1 -sASYNCIFY_STACK_SIZE=65536 -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' -sPTHREAD_POOL_SIZE_STRICT=0 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB -sMAX_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','dynCall'] -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['\$dynCall'] --bind -L${SYSROOT}/lib ${STUBS_BUILD}/libgit2_stub.a ${STUBS_BUILD}/libcurl_stub.a ${STUBS_BUILD}/libpcbnew_scripting_stub.a ${STUBS_BUILD}/libnng_stub.a ${STUBS_BUILD}/pcbnew_embind.o" \
|
|
||||||
-DCMAKE_PREFIX_PATH="${SYSROOT};${WX_BUILD}" \
|
|
||||||
-DwxWidgets_CONFIG_EXECUTABLE="${WX_BUILD}/wx-config" \
|
|
||||||
\
|
|
||||||
-DKICAD_BUILD_QA_TESTS=OFF \
|
|
||||||
-DKICAD_SPICE=OFF \
|
|
||||||
-DKICAD_USE_EGL=OFF \
|
|
||||||
-DKICAD_USE_BUNDLED_GLEW=ON \
|
|
||||||
-DKICAD_BUILD_3D_VIEWER_WASM=OFF \
|
|
||||||
-DKICAD_IPC_API=ON \
|
|
||||||
\
|
|
||||||
-DZSTD_ROOT="${SYSROOT}" \
|
|
||||||
-DZSTD_INCLUDE_DIR="${SYSROOT}/include" \
|
|
||||||
-DZSTD_LIBRARY="${SYSROOT}/lib/libzstd.a" \
|
|
||||||
-DGLM_INCLUDE_DIR="${SYSROOT}/include" \
|
|
||||||
-DGLM_VERSION="0.9.9.8" \
|
|
||||||
-DBOOST_ROOT="${SYSROOT}" \
|
|
||||||
-DBoost_INCLUDE_DIR="${SYSROOT}/include" \
|
|
||||||
-DBoost_LIBRARY_DIR="${SYSROOT}/lib" \
|
|
||||||
-DBoost_NO_SYSTEM_PATHS=ON \
|
|
||||||
-DBoost_NO_BOOST_CMAKE=ON \
|
|
||||||
-DFREETYPE_INCLUDE_DIR_ft2build="${SYSROOT}/include/freetype2" \
|
|
||||||
-DFREETYPE_INCLUDE_DIR_freetype2="${SYSROOT}/include/freetype2" \
|
|
||||||
-DFREETYPE_LIBRARY="${SYSROOT}/lib/libfreetype.a" \
|
|
||||||
-DHarfBuzz_INCLUDE_DIR="${SYSROOT}/include/harfbuzz" \
|
|
||||||
-DHarfBuzz_LIBRARY="${SYSROOT}/lib/libharfbuzz.a" \
|
|
||||||
-DOCC_INCLUDE_DIR="${SYSROOT}/include/opencascade" \
|
|
||||||
-DOCC_LIBRARY_DIR="${SYSROOT}/lib" \
|
|
||||||
-DProtobuf_INCLUDE_DIR="${SYSROOT}/include" \
|
|
||||||
-DProtobuf_LIBRARY="${SYSROOT}/lib/libprotobuf.a" \
|
|
||||||
-DProtobuf_LITE_LIBRARY="${SYSROOT}/lib/libprotobuf-lite.a" \
|
|
||||||
-DProtobuf_PROTOC_EXECUTABLE="${SYSROOT}/bin/protoc" \
|
|
||||||
-DODBC_CONFIG:STRING="stub-for-wasm" \
|
|
||||||
-DODBCLIB:STRING="" \
|
|
||||||
-DODBC_CFLAGS:STRING="" \
|
|
||||||
-DODBC_LINK_FLAGS:STRING="" \
|
|
||||||
-DODBC_LIBRARIES:STRING="" \
|
|
||||||
\
|
|
||||||
-DBUILD_GITHUB_PLUGIN=OFF \
|
|
||||||
-DKICAD_PCM=OFF \
|
|
||||||
\
|
|
||||||
-DHAVE_STRCASECMP=1 \
|
|
||||||
-DHAVE_STRNCASECMP=1
|
|
||||||
|
|
||||||
# Step 7.1: Compile Embind bindings (after CMake so config.h exists)
|
|
||||||
# Exposes KiCad objects to JavaScript for future Pyodide integration
|
|
||||||
EMBIND_SRC="${PROJECT_ROOT}/wasm/bindings/pcbnew_embind.cpp"
|
|
||||||
if [ -f "$EMBIND_SRC" ]; then
|
|
||||||
log_info "Compiling Embind bindings..."
|
|
||||||
# Use the same includes and flags that KiCad uses
|
|
||||||
KICAD_INCLUDES="-I${KICAD_BUILD} -I${KICAD_DIR}/include -I${KICAD_DIR}/pcbnew -I${KICAD_DIR}/common"
|
|
||||||
KICAD_INCLUDES+=" -I${KICAD_DIR}/libs/core/include -I${KICAD_DIR}/libs/kimath/include -I${KICAD_DIR}/libs/kiplatform/include"
|
|
||||||
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/clipper2/Clipper2Lib/include"
|
|
||||||
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/nlohmann_json"
|
|
||||||
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/dynamic_bitset"
|
|
||||||
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/nanodbc"
|
|
||||||
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/picosha2"
|
|
||||||
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty"
|
|
||||||
KICAD_INCLUDES+=" -I${SYSROOT}/include"
|
|
||||||
# KiCad requires C++20 for concepts
|
|
||||||
em++ -std=c++20 -c ${EXTRA_FLAGS} ${WX_CXXFLAGS} ${KICAD_INCLUDES} "$EMBIND_SRC" -o "${STUBS_BUILD}/pcbnew_embind.o"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 8: Build pcbnew target
|
|
||||||
log_info "Building pcbnew..."
|
|
||||||
emmake make -j${JOBS} pcbnew
|
|
||||||
|
|
||||||
# Step 8.1: Build bitmap resources (images.tar.gz)
|
|
||||||
# This creates the icon archive that KiCad loads at runtime
|
|
||||||
log_info "Building bitmap resources..."
|
|
||||||
emmake make bitmap_archive_build
|
|
||||||
|
|
||||||
# Step 9: Create stamp file
|
|
||||||
create_stamp "${KICAD_STAMP}"
|
|
||||||
log_info "KiCad PCBnew build complete!"
|
|
||||||
log_info "Output: ${KICAD_BUILD}/pcbnew/pcbnew.js"
|
|
||||||
|
|
|
||||||
199
tests/apps/kicad/eeschema.html
Normal file
199
tests/apps/kicad/eeschema.html
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-us">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<title>KiCad Eeschema WASM</title>
|
||||||
|
<style>
|
||||||
|
.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
|
||||||
|
div.emscripten { text-align: center; }
|
||||||
|
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
|
||||||
|
canvas.emscripten { border: 0px none; }
|
||||||
|
|
||||||
|
.window {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: black;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: monospace;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(0,0,0,0.7);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress {
|
||||||
|
width: 300px;
|
||||||
|
height: 20px;
|
||||||
|
background: #333;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: #4CAF50;
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #1a1a2e;">
|
||||||
|
<div id="main-window" style="width: 100vw; height: 100vh; position: absolute; top: 0; left: 0;"></div>
|
||||||
|
|
||||||
|
<div id="status">
|
||||||
|
<div id="status-text">Initializing...</div>
|
||||||
|
<div id="progress"><div id="progress-bar"></div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="window-container"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var mainWindow = document.getElementById('main-window');
|
||||||
|
var statusText = document.getElementById('status-text');
|
||||||
|
var progressBar = document.getElementById('progress-bar');
|
||||||
|
|
||||||
|
var showError = function(msg) {
|
||||||
|
console.error('[KICAD_ERROR] ' + msg);
|
||||||
|
statusText.textContent = 'Error: ' + msg;
|
||||||
|
statusText.style.color = 'red';
|
||||||
|
};
|
||||||
|
|
||||||
|
var createCanvas = function() {
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.id = 'canvas';
|
||||||
|
canvas.style.display = 'none';
|
||||||
|
// wx.js owns the backing-store size via setWindowRect(); keep the HTML
|
||||||
|
// shell responsible only for the CSS size.
|
||||||
|
var width = window.innerWidth;
|
||||||
|
var height = window.innerHeight;
|
||||||
|
canvas.style.width = width + 'px';
|
||||||
|
canvas.style.height = height + 'px';
|
||||||
|
canvas.oncontextmenu = function() { event.preventDefault(); };
|
||||||
|
canvas.addEventListener("webglcontextlost", function(e) {
|
||||||
|
showError('WebGL context lost. You will need to reload the page.');
|
||||||
|
e.preventDefault();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
mainWindow.appendChild(canvas);
|
||||||
|
Module.canvas = canvas;
|
||||||
|
|
||||||
|
console.log('[KICAD] preRun complete, canvas created: ' + width + 'x' + height);
|
||||||
|
};
|
||||||
|
|
||||||
|
var onRuntimeInitialized = function() {
|
||||||
|
console.log('[KICAD] Runtime initialized');
|
||||||
|
var canvas = Module.canvas;
|
||||||
|
canvas.style.display = 'block';
|
||||||
|
document.getElementById('status').style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pre-fetched resource data (fetched before eeschema.js loads)
|
||||||
|
var resourceData = null;
|
||||||
|
|
||||||
|
// Start fetching images.tar.gz immediately (runs in parallel with WASM loading)
|
||||||
|
fetch('images.tar.gz')
|
||||||
|
.then(function(response) {
|
||||||
|
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||||
|
return response.arrayBuffer();
|
||||||
|
})
|
||||||
|
.then(function(buffer) {
|
||||||
|
resourceData = new Uint8Array(buffer);
|
||||||
|
console.log('[KICAD] Prefetched images.tar.gz (' + resourceData.length + ' bytes)');
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.warn('[KICAD] Could not prefetch images.tar.gz:', err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write pre-fetched resources to FS (called in preRun after FS is available)
|
||||||
|
var writeResources = function() {
|
||||||
|
// Create directory structure matching KiCad's compiled-in KICAD_DATA path
|
||||||
|
// This path is baked in during CMake configuration
|
||||||
|
var resourcePath = '/workspace/build-wasm/sysroot/share/kicad/resources';
|
||||||
|
FS.mkdirTree(resourcePath);
|
||||||
|
|
||||||
|
// Write pre-fetched data if available
|
||||||
|
if (resourceData) {
|
||||||
|
FS.writeFile(resourcePath + '/images.tar.gz', resourceData);
|
||||||
|
console.log('[KICAD] Wrote images.tar.gz to ' + resourcePath);
|
||||||
|
} else {
|
||||||
|
console.warn('[KICAD] images.tar.gz not ready yet (WASM loaded faster than fetch)');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var Module = {
|
||||||
|
thisProgram: '/usr/bin/eeschema', // Fake absolute path for argv[0] (KiCad DEBUG check)
|
||||||
|
|
||||||
|
preRun: [createCanvas, writeResources],
|
||||||
|
postRun: [],
|
||||||
|
|
||||||
|
print: function(text) {
|
||||||
|
if (arguments.length > 1)
|
||||||
|
text = Array.prototype.slice.call(arguments).join(' ');
|
||||||
|
console.log('[KICAD_OUT] ' + text);
|
||||||
|
},
|
||||||
|
|
||||||
|
printErr: function(text) {
|
||||||
|
if (arguments.length > 1)
|
||||||
|
text = Array.prototype.slice.call(arguments).join(' ');
|
||||||
|
console.error('[KICAD_ERR] ' + text);
|
||||||
|
},
|
||||||
|
|
||||||
|
setStatus: function(text) {
|
||||||
|
console.log('[KICAD_STATUS] ' + text);
|
||||||
|
statusText.textContent = text;
|
||||||
|
|
||||||
|
// Parse progress from status text
|
||||||
|
var match = text.match(/(\d+)\/(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
var pct = (parseInt(match[1]) / parseInt(match[2])) * 100;
|
||||||
|
progressBar.style.width = pct + '%';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
totalDependencies: 0,
|
||||||
|
monitorRunDependencies: function(left) {
|
||||||
|
this.totalDependencies = Math.max(this.totalDependencies, left);
|
||||||
|
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
|
||||||
|
},
|
||||||
|
|
||||||
|
onRuntimeInitialized: onRuntimeInitialized,
|
||||||
|
|
||||||
|
// Required for locating .wasm and .worker.js files
|
||||||
|
locateFile: function(path) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.setStatus('Downloading...');
|
||||||
|
|
||||||
|
window.onerror = function(msg, url, line) {
|
||||||
|
showError(msg + ' at ' + url + ':' + line);
|
||||||
|
Module.setStatus = function(text) {
|
||||||
|
if (text) Module.printErr('[post-exception status] ' + text);
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- wxWidgets WASM glue code (defines getConfigEntryLength, etc.) -->
|
||||||
|
<script src="wx.js"></script>
|
||||||
|
<script async src="eeschema.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
492
tests/kicad/eeschema.spec.ts
Normal file
492
tests/kicad/eeschema.spec.ts
Normal file
|
|
@ -0,0 +1,492 @@
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { test, expect } from './fixtures';
|
||||||
|
import { clickByLabel, clickByTooltip, findByTooltip } from '../e2e/utils/element-tracker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eeschema (schematic editor) WASM E2E Tests
|
||||||
|
*
|
||||||
|
* Mirrors pcbnew.spec.ts. The wxWidgets setup wizard is shared infrastructure,
|
||||||
|
* so the wizard flow is identical. Editor-specific checks (Appearance pane,
|
||||||
|
* exact toolbar count, reference-image diff, etc.) are intentionally omitted
|
||||||
|
* here until the eeschema UI surface is empirically pinned down.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type CanvasMetrics = {
|
||||||
|
dpr: number;
|
||||||
|
mainCanvas: null | {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
rectWidth: number;
|
||||||
|
rectHeight: number;
|
||||||
|
};
|
||||||
|
glCanvas: null | {
|
||||||
|
id: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
rectWidth: number;
|
||||||
|
rectHeight: number;
|
||||||
|
viewport: number[] | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type RegistryMetrics = {
|
||||||
|
elementStats: null | {
|
||||||
|
total: number;
|
||||||
|
byType: Record<string, number>;
|
||||||
|
};
|
||||||
|
renderedStats: null | {
|
||||||
|
total: number;
|
||||||
|
byType: Record<string, number>;
|
||||||
|
};
|
||||||
|
toolbars: Array<{
|
||||||
|
id: string;
|
||||||
|
typeName: string;
|
||||||
|
screenX: number;
|
||||||
|
screenY: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DiffRegion = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ScreenshotDifference = {
|
||||||
|
actualWidth: number;
|
||||||
|
actualHeight: number;
|
||||||
|
diffPixels: number;
|
||||||
|
diffRatio: number;
|
||||||
|
meanChannelDiff: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function compareScreenshots(
|
||||||
|
page: Page,
|
||||||
|
beforePng: Buffer,
|
||||||
|
afterPng: Buffer,
|
||||||
|
region: DiffRegion
|
||||||
|
): Promise<ScreenshotDifference> {
|
||||||
|
return page.evaluate(async ({ beforeBase64, afterBase64, crop }) => {
|
||||||
|
const loadImage = async (base64: string): Promise<HTMLImageElement> => {
|
||||||
|
const image = new Image();
|
||||||
|
image.src = `data:image/png;base64,${base64}`;
|
||||||
|
await image.decode();
|
||||||
|
return image;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [before, after] = await Promise.all([
|
||||||
|
loadImage(beforeBase64),
|
||||||
|
loadImage(afterBase64),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (before.width !== after.width || before.height !== after.height) {
|
||||||
|
return {
|
||||||
|
actualWidth: after.width,
|
||||||
|
actualHeight: after.height,
|
||||||
|
diffPixels: Number.POSITIVE_INFINITY,
|
||||||
|
diffRatio: Number.POSITIVE_INFINITY,
|
||||||
|
meanChannelDiff: Number.POSITIVE_INFINITY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = crop.width;
|
||||||
|
canvas.height = crop.height;
|
||||||
|
|
||||||
|
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('2D canvas context unavailable for screenshot comparison');
|
||||||
|
}
|
||||||
|
|
||||||
|
context.drawImage(before, crop.x, crop.y, crop.width, crop.height, 0, 0, crop.width, crop.height);
|
||||||
|
const beforeData = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||||
|
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
context.drawImage(after, crop.x, crop.y, crop.width, crop.height, 0, 0, crop.width, crop.height);
|
||||||
|
const afterData = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||||
|
|
||||||
|
let diffPixels = 0;
|
||||||
|
let totalChannelDiff = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < beforeData.length; i += 4) {
|
||||||
|
const dr = Math.abs(beforeData[i] - afterData[i]);
|
||||||
|
const dg = Math.abs(beforeData[i + 1] - afterData[i + 1]);
|
||||||
|
const db = Math.abs(beforeData[i + 2] - afterData[i + 2]);
|
||||||
|
const da = Math.abs(beforeData[i + 3] - afterData[i + 3]);
|
||||||
|
const maxDiff = Math.max(dr, dg, db, da);
|
||||||
|
|
||||||
|
totalChannelDiff += dr + dg + db + da;
|
||||||
|
|
||||||
|
if (maxDiff > 16) {
|
||||||
|
diffPixels += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
actualWidth: after.width,
|
||||||
|
actualHeight: after.height,
|
||||||
|
diffPixels,
|
||||||
|
diffRatio: diffPixels / (canvas.width * canvas.height),
|
||||||
|
meanChannelDiff: totalChannelDiff / beforeData.length,
|
||||||
|
};
|
||||||
|
}, {
|
||||||
|
beforeBase64: beforePng.toString('base64'),
|
||||||
|
afterBase64: afterPng.toString('base64'),
|
||||||
|
crop: region,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCanvasMetrics(page: Page): Promise<CanvasMetrics> {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const mainCanvas = document.querySelector('#canvas') as HTMLCanvasElement | null;
|
||||||
|
const glCanvas =
|
||||||
|
Array.from(document.querySelectorAll('[id^="glcanvas-"]'))
|
||||||
|
.map((canvas) => canvas as HTMLCanvasElement)
|
||||||
|
.find((canvas) => {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const style = window.getComputedStyle(canvas);
|
||||||
|
return style.display !== 'none' && rect.width > 0 && rect.height > 0;
|
||||||
|
}) ??
|
||||||
|
document.querySelector('[id^="glcanvas-"]') as HTMLCanvasElement | null;
|
||||||
|
|
||||||
|
const mainRect = mainCanvas?.getBoundingClientRect();
|
||||||
|
const glRect = glCanvas?.getBoundingClientRect();
|
||||||
|
const gl =
|
||||||
|
glCanvas?.getContext('webgl2') ||
|
||||||
|
glCanvas?.getContext('webgl');
|
||||||
|
const viewport = gl ? Array.from(gl.getParameter(gl.VIEWPORT) as Int32Array | number[]) : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
dpr,
|
||||||
|
mainCanvas: mainCanvas && mainRect ? {
|
||||||
|
width: mainCanvas.width,
|
||||||
|
height: mainCanvas.height,
|
||||||
|
rectWidth: mainRect.width,
|
||||||
|
rectHeight: mainRect.height,
|
||||||
|
} : null,
|
||||||
|
glCanvas: glCanvas && glRect ? {
|
||||||
|
id: glCanvas.id,
|
||||||
|
width: glCanvas.width,
|
||||||
|
height: glCanvas.height,
|
||||||
|
rectWidth: glRect.width,
|
||||||
|
rectHeight: glRect.height,
|
||||||
|
viewport,
|
||||||
|
} : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRegistryMetrics(page: Page): Promise<RegistryMetrics> {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const registry = window.wxElementRegistry;
|
||||||
|
|
||||||
|
if (!registry) {
|
||||||
|
return {
|
||||||
|
elementStats: null,
|
||||||
|
renderedStats: null,
|
||||||
|
toolbars: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const allElements = registry.findAll({ visible: true });
|
||||||
|
const toolbars = allElements
|
||||||
|
.filter((element) => /ToolBar/.test(element.typeName))
|
||||||
|
.map((element) => ({
|
||||||
|
id: element.id,
|
||||||
|
typeName: element.typeName,
|
||||||
|
screenX: element.screenX,
|
||||||
|
screenY: element.screenY,
|
||||||
|
width: element.width,
|
||||||
|
height: element.height,
|
||||||
|
label: element.label,
|
||||||
|
name: element.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
elementStats: registry.getStats(),
|
||||||
|
renderedStats: registry.getRenderedStats ? registry.getRenderedStats() : null,
|
||||||
|
toolbars,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeWizard(page: Page): Promise<void> {
|
||||||
|
await expect(page.locator('#canvas')).toBeVisible({ timeout: 90000 });
|
||||||
|
await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: 90000 });
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'test-results/eeschema-wizard-00-initial.png', scale: 'device' });
|
||||||
|
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
let clicked = await clickByLabel(page, 'Next >');
|
||||||
|
|
||||||
|
if (!clicked) {
|
||||||
|
clicked = await clickByLabel(page, 'Finish');
|
||||||
|
|
||||||
|
if (clicked) {
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await page.screenshot({
|
||||||
|
path: `test-results/eeschema-wizard-${String(i).padStart(2, '0')}-finish.png`,
|
||||||
|
scale: 'device'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await page.screenshot({
|
||||||
|
path: `test-results/eeschema-wizard-${String(i).padStart(2, '0')}.png`,
|
||||||
|
scale: 'device'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hideCursor(page: Page): Promise<void> {
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document.documentElement.style.cursor = 'none';
|
||||||
|
document.body.style.cursor = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('Eeschema WASM', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/kicad/eeschema.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('click through setup wizard to load Eeschema', async ({ page }) => {
|
||||||
|
await completeWizard(page);
|
||||||
|
const metrics = await getCanvasMetrics(page);
|
||||||
|
const registryMetrics = await getRegistryMetrics(page);
|
||||||
|
|
||||||
|
// Headless Firefox runs at dpr=1; pcbnew's stricter `> 1` check assumes a
|
||||||
|
// Retina-aware run. The eeschema MVP just needs to verify dpr is sane.
|
||||||
|
expect(metrics.dpr).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(metrics.mainCanvas).not.toBeNull();
|
||||||
|
expect(metrics.glCanvas).not.toBeNull();
|
||||||
|
expect(registryMetrics.toolbars.length).toBeGreaterThanOrEqual(2);
|
||||||
|
|
||||||
|
if (!metrics.mainCanvas || !metrics.glCanvas) {
|
||||||
|
throw new Error('KiCad canvases not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(Math.round(metrics.mainCanvas.rectWidth * metrics.dpr)).toBe(metrics.mainCanvas.width);
|
||||||
|
expect(Math.round(metrics.mainCanvas.rectHeight * metrics.dpr)).toBe(metrics.mainCanvas.height);
|
||||||
|
expect(metrics.glCanvas.rectWidth).toBeGreaterThan(800);
|
||||||
|
expect(metrics.glCanvas.rectHeight).toBeGreaterThan(500);
|
||||||
|
expect(Math.round(metrics.glCanvas.rectWidth * metrics.dpr)).toBe(metrics.glCanvas.width);
|
||||||
|
expect(Math.round(metrics.glCanvas.rectHeight * metrics.dpr)).toBe(metrics.glCanvas.height);
|
||||||
|
|
||||||
|
const viewport = metrics.glCanvas.viewport;
|
||||||
|
expect(viewport).not.toBeNull();
|
||||||
|
|
||||||
|
if (!viewport) {
|
||||||
|
throw new Error('WebGL viewport unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(viewport[2]).toBe(metrics.glCanvas.width);
|
||||||
|
expect(viewport[3]).toBe(metrics.glCanvas.height);
|
||||||
|
|
||||||
|
await hideCursor(page);
|
||||||
|
|
||||||
|
// Capture a CSS-scale screenshot for visual review; no reference image
|
||||||
|
// is wired up yet (eeschema's chrome differs enough from pcbnew that
|
||||||
|
// sharing pcbnew's baseline isn't viable). Add a dedicated baseline
|
||||||
|
// here once the layout is finalised.
|
||||||
|
await page.screenshot({
|
||||||
|
path: 'test-results/eeschema-loaded-css.png',
|
||||||
|
scale: 'css'
|
||||||
|
});
|
||||||
|
await page.screenshot({ path: 'test-results/eeschema-loaded.png', scale: 'device' });
|
||||||
|
|
||||||
|
const canvasCount = await page.locator('canvas').count();
|
||||||
|
expect(canvasCount).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('select draw wires and draw on the schematic', async ({ page, testLogger }) => {
|
||||||
|
await completeWizard(page);
|
||||||
|
await hideCursor(page);
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const canvases = Array.from(document.querySelectorAll('canvas')).map((canvas) => {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const style = window.getComputedStyle(canvas);
|
||||||
|
return {
|
||||||
|
id: canvas.id,
|
||||||
|
className: canvas.className,
|
||||||
|
display: style.display,
|
||||||
|
visibility: style.visibility,
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height,
|
||||||
|
rectX: rect.x,
|
||||||
|
rectY: rect.y,
|
||||||
|
rectWidth: rect.width,
|
||||||
|
rectHeight: rect.height,
|
||||||
|
shouldBeVisible: (canvas as HTMLCanvasElement).dataset?.shouldBeVisible ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[TEST] canvas summary ${JSON.stringify(canvases)}`);
|
||||||
|
|
||||||
|
const registry = window.wxElementRegistry;
|
||||||
|
const topLevels = (registry?.findAll?.({}) ?? [])
|
||||||
|
.filter((item) => /Frame|Dialog|Wizard/.test(item.typeName))
|
||||||
|
.slice(0, 20)
|
||||||
|
.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
typeName: item.typeName,
|
||||||
|
label: item.label,
|
||||||
|
name: item.name,
|
||||||
|
visible: item.visible,
|
||||||
|
enabled: item.enabled,
|
||||||
|
screenX: item.screenX,
|
||||||
|
screenY: item.screenY,
|
||||||
|
width: item.width,
|
||||||
|
height: item.height,
|
||||||
|
}));
|
||||||
|
const rendered = registry?.findAllRendered?.({}) ?? [];
|
||||||
|
const byType = rendered.reduce<Record<string, number>>((acc, item) => {
|
||||||
|
acc[item.elementType] = (acc[item.elementType] ?? 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
const tools = rendered
|
||||||
|
.filter((item) => item.elementType === 'tool')
|
||||||
|
.slice(0, 20)
|
||||||
|
.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
label: item.label,
|
||||||
|
tooltip: item.tooltip,
|
||||||
|
checked: item.checked,
|
||||||
|
enabled: item.enabled,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(`[TEST] top-level summary ${JSON.stringify(topLevels)}`);
|
||||||
|
console.log(`[TEST] rendered summary ${JSON.stringify({ count: rendered.length, byType, tools })}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const registry = window.wxElementRegistry;
|
||||||
|
if (!registry?.findAllRendered) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return registry.findAllRendered({ elementType: 'tool' })
|
||||||
|
.some((tool) => tool.tooltip?.includes('Draw Wires'));
|
||||||
|
}, null, { timeout: 15000 });
|
||||||
|
|
||||||
|
const drawWiresTool = await findByTooltip(page, 'Draw Wires', { elementType: 'tool' });
|
||||||
|
expect(drawWiresTool).not.toBeNull();
|
||||||
|
|
||||||
|
if (!drawWiresTool) {
|
||||||
|
throw new Error('Draw Wires tool not found in rendered element registry');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The registry carries checked state via a " [checked]" label suffix
|
||||||
|
// appended by wxAuiToolBar::OnPaint on Emscripten — no schema change.
|
||||||
|
const isToolChecked = (t: { label?: string } | null | undefined) =>
|
||||||
|
(t?.label ?? '').includes('[checked]');
|
||||||
|
|
||||||
|
expect(drawWiresTool.enabled).toBe(true);
|
||||||
|
expect(isToolChecked(drawWiresTool)).toBe(false);
|
||||||
|
const baselineErrorCount = testLogger.errors.length;
|
||||||
|
|
||||||
|
await page.screenshot({
|
||||||
|
path: 'test-results/eeschema-draw-wires-00-before-tool-click.png',
|
||||||
|
scale: 'device'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await clickByTooltip(page, 'Draw Wires', { elementType: 'tool' })).toBe(true);
|
||||||
|
|
||||||
|
await expect.poll(async () => {
|
||||||
|
const tool = await findByTooltip(page, 'Draw Wires', { elementType: 'tool' });
|
||||||
|
return isToolChecked(tool);
|
||||||
|
}, {
|
||||||
|
message: 'Draw Wires tool should stay selected after the click',
|
||||||
|
timeout: 5000,
|
||||||
|
}).toBe(true);
|
||||||
|
|
||||||
|
await page.mouse.move(640, 360);
|
||||||
|
await page.waitForTimeout(600);
|
||||||
|
|
||||||
|
const selectedDrawWiresTool = await findByTooltip(page, 'Draw Wires', { elementType: 'tool' });
|
||||||
|
expect(isToolChecked(selectedDrawWiresTool)).toBe(true);
|
||||||
|
|
||||||
|
const afterToolClick = await page.screenshot({
|
||||||
|
path: 'test-results/eeschema-draw-wires-01-after-click.png',
|
||||||
|
scale: 'device'
|
||||||
|
});
|
||||||
|
|
||||||
|
const glCanvasId = await page.evaluate(() => {
|
||||||
|
const glCanvas =
|
||||||
|
Array.from(document.querySelectorAll('[id^="glcanvas-"]'))
|
||||||
|
.map((canvas) => canvas as HTMLCanvasElement)
|
||||||
|
.find((canvas) => {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const style = window.getComputedStyle(canvas);
|
||||||
|
return style.display !== 'none' && rect.width > 0 && rect.height > 0;
|
||||||
|
}) ??
|
||||||
|
document.querySelector('[id^="glcanvas-"]') as HTMLCanvasElement | null;
|
||||||
|
|
||||||
|
return glCanvas?.id ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(glCanvasId).not.toBeNull();
|
||||||
|
|
||||||
|
if (!glCanvasId) {
|
||||||
|
throw new Error('Visible GL canvas not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const glCanvasBox = await page.locator(`#${glCanvasId}`).boundingBox();
|
||||||
|
expect(glCanvasBox).not.toBeNull();
|
||||||
|
|
||||||
|
if (!glCanvasBox) {
|
||||||
|
throw new Error('GL canvas bounding box unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPoint = {
|
||||||
|
x: Math.round(glCanvasBox.x + glCanvasBox.width * 0.28),
|
||||||
|
y: Math.round(glCanvasBox.y + glCanvasBox.height * 0.36),
|
||||||
|
};
|
||||||
|
const endPoint = {
|
||||||
|
x: Math.round(glCanvasBox.x + glCanvasBox.width * 0.48),
|
||||||
|
y: Math.round(glCanvasBox.y + glCanvasBox.height * 0.47),
|
||||||
|
};
|
||||||
|
|
||||||
|
await page.mouse.click(startPoint.x, startPoint.y);
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
await page.mouse.click(endPoint.x, endPoint.y);
|
||||||
|
await page.waitForTimeout(750);
|
||||||
|
|
||||||
|
const afterDrawing = await page.screenshot({
|
||||||
|
path: 'test-results/eeschema-draw-wires-02-after-drawing.png',
|
||||||
|
scale: 'device'
|
||||||
|
});
|
||||||
|
|
||||||
|
const diffRegion: DiffRegion = {
|
||||||
|
x: Math.max(0, Math.min(startPoint.x, endPoint.x) - 24),
|
||||||
|
y: Math.max(0, Math.min(startPoint.y, endPoint.y) - 24),
|
||||||
|
width: Math.abs(endPoint.x - startPoint.x) + 48,
|
||||||
|
height: Math.abs(endPoint.y - startPoint.y) + 48,
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawingDiff = await compareScreenshots(page, afterToolClick, afterDrawing, diffRegion);
|
||||||
|
|
||||||
|
expect(drawingDiff.diffPixels).toBeGreaterThan(120);
|
||||||
|
expect(drawingDiff.diffRatio).toBeGreaterThan(0.01);
|
||||||
|
expect(drawingDiff.meanChannelDiff).toBeGreaterThan(1);
|
||||||
|
|
||||||
|
const realErrors = testLogger.errors
|
||||||
|
.slice(baselineErrorCount)
|
||||||
|
.filter((error) => !error.includes('favicon') && !error.includes('uncaught exception: unwind'));
|
||||||
|
expect(realErrors).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -9,15 +9,21 @@
|
||||||
"build-wasm": "cd apps && make -f Makefile.wasm",
|
"build-wasm": "cd apps && make -f Makefile.wasm",
|
||||||
"serve": "npx serve apps -p 8080 -c ../serve.json",
|
"serve": "npx serve apps -p 8080 -c ../serve.json",
|
||||||
"setup:kicad": "./scripts/setup-kicad-wasm.sh",
|
"setup:kicad": "./scripts/setup-kicad-wasm.sh",
|
||||||
|
"setup:calculator": "./scripts/setup-calculator-wasm.sh",
|
||||||
"test:kicad:firefox": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=firefox",
|
"test:kicad:firefox": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=firefox",
|
||||||
"test:kicad:chrome": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed",
|
"test:kicad:chrome": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed",
|
||||||
"test:kicad": "npm run test:kicad:firefox",
|
"test:kicad": "npm run test:kicad:firefox",
|
||||||
"test:kicad:headed": "npm run test:kicad:chrome",
|
"test:kicad:headed": "npm run test:kicad:chrome",
|
||||||
"setup:calculator": "./scripts/setup-calculator-wasm.sh",
|
"test:pcbnew:firefox": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=firefox kicad/pcbnew.spec.ts",
|
||||||
|
"test:pcbnew:chrome": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed kicad/pcbnew.spec.ts",
|
||||||
"test:calculator:firefox": "npm run setup:calculator && playwright test --config=playwright-calculator.config.ts --project=firefox",
|
"test:calculator:firefox": "npm run setup:calculator && playwright test --config=playwright-calculator.config.ts --project=firefox",
|
||||||
"test:calculator:chrome": "npm run setup:calculator && playwright test --config=playwright-calculator.config.ts --project=chromium --headed",
|
"test:calculator:chrome": "npm run setup:calculator && playwright test --config=playwright-calculator.config.ts --project=chromium --headed",
|
||||||
"test:calculator": "npm run test:calculator:firefox",
|
"test:calculator": "npm run test:calculator:firefox",
|
||||||
"test:calculator:headed": "npm run test:calculator:chrome",
|
"test:calculator:headed": "npm run test:calculator:chrome",
|
||||||
|
"test:eeschema:firefox": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=firefox kicad/eeschema.spec.ts",
|
||||||
|
"test:eeschema:chrome": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed kicad/eeschema.spec.ts",
|
||||||
|
"test:eeschema": "npm run test:eeschema:firefox",
|
||||||
|
"test:eeschema:headed": "npm run test:eeschema:chrome",
|
||||||
"test:coroutine:firefox": "playwright test --config=playwright-coroutine.config.ts --project=firefox",
|
"test:coroutine:firefox": "playwright test --config=playwright-coroutine.config.ts --project=firefox",
|
||||||
"test:coroutine:chrome": "playwright test --config=playwright-coroutine.config.ts --project=chromium --headed"
|
"test:coroutine:chrome": "playwright test --config=playwright-coroutine.config.ts --project=chromium --headed"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
#
|
#
|
||||||
# Priority: Use local output/ directory (populated by docker/build.sh)
|
# Priority: Use local output/ directory (populated by docker/build.sh)
|
||||||
# Fallback: Copy from Docker volume directly
|
# Fallback: Copy from Docker volume directly
|
||||||
|
#
|
||||||
|
# Copies whichever editors are present (pcbnew, eeschema).
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
|
@ -13,32 +15,46 @@ OUTPUT_DIR="$PROJECT_ROOT/output"
|
||||||
|
|
||||||
mkdir -p "$KICAD_TEST"
|
mkdir -p "$KICAD_TEST"
|
||||||
|
|
||||||
# Check if output directory has the build files
|
# Copy one editor's artifacts (js, wasm, optional debug/map/worker). Returns 0
|
||||||
if [ -f "$OUTPUT_DIR/pcbnew.js" ] && [ -f "$OUTPUT_DIR/pcbnew.wasm" ]; then
|
# if the editor was present, 1 if neither output/ nor the docker volume has it.
|
||||||
echo "Copying KiCad WASM files from output directory..."
|
copy_app() {
|
||||||
cp "$OUTPUT_DIR/pcbnew.js" "$KICAD_TEST/"
|
local app="$1"
|
||||||
cp "$OUTPUT_DIR/pcbnew.wasm" "$KICAD_TEST/"
|
|
||||||
# Source map for debug symbols (optional)
|
if [ -f "$OUTPUT_DIR/${app}.js" ] && [ -f "$OUTPUT_DIR/${app}.wasm" ]; then
|
||||||
cp "$OUTPUT_DIR/pcbnew.wasm.map" "$KICAD_TEST/" 2>/dev/null || true
|
echo "Copying ${app} WASM files from output directory..."
|
||||||
# Worker file for pthreads (optional)
|
cp "$OUTPUT_DIR/${app}.js" "$KICAD_TEST/"
|
||||||
cp "$OUTPUT_DIR/pcbnew.worker.js" "$KICAD_TEST/" 2>/dev/null || true
|
cp "$OUTPUT_DIR/${app}.wasm" "$KICAD_TEST/"
|
||||||
# Bitmap resources for KiCad icons (optional)
|
cp "$OUTPUT_DIR/${app}.wasm.map" "$KICAD_TEST/" 2>/dev/null || true
|
||||||
cp "$OUTPUT_DIR/images.tar.gz" "$KICAD_TEST/" 2>/dev/null || true
|
cp "$OUTPUT_DIR/${app}.worker.js" "$KICAD_TEST/" 2>/dev/null || true
|
||||||
else
|
cp "$OUTPUT_DIR/images.tar.gz" "$KICAD_TEST/" 2>/dev/null || true
|
||||||
echo "Output directory not found, copying from Docker build..."
|
return 0
|
||||||
docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
fi
|
||||||
kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.js "$KICAD_TEST/"
|
|
||||||
docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
echo "Output ${app} not found locally, trying Docker volume..."
|
||||||
kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.wasm "$KICAD_TEST/"
|
if docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
||||||
# Source map for debug symbols (optional)
|
kicad-wasm-builder:/workspace/build-wasm/kicad-${app}/${app}/${app}.js "$KICAD_TEST/" 2>/dev/null \
|
||||||
docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
&& docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
||||||
kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.wasm.map "$KICAD_TEST/" 2>/dev/null || true
|
kicad-wasm-builder:/workspace/build-wasm/kicad-${app}/${app}/${app}.wasm "$KICAD_TEST/" 2>/dev/null; then
|
||||||
# Worker file for pthreads (optional)
|
docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
||||||
docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
kicad-wasm-builder:/workspace/build-wasm/kicad-${app}/${app}/${app}.wasm.map "$KICAD_TEST/" 2>/dev/null || true
|
||||||
kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.worker.js "$KICAD_TEST/" 2>/dev/null || true
|
docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
||||||
# Bitmap resources for KiCad icons (optional)
|
kicad-wasm-builder:/workspace/build-wasm/kicad-${app}/${app}/${app}.worker.js "$KICAD_TEST/" 2>/dev/null || true
|
||||||
docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
||||||
kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/resources/images.tar.gz "$KICAD_TEST/" 2>/dev/null || true
|
kicad-wasm-builder:/workspace/build-wasm/kicad-${app}/resources/images.tar.gz "$KICAD_TEST/" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " (no ${app} artifacts found — skipping)"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
found_any=0
|
||||||
|
copy_app pcbnew && found_any=1
|
||||||
|
copy_app eeschema && found_any=1
|
||||||
|
|
||||||
|
if [ "$found_any" -eq 0 ]; then
|
||||||
|
echo "Error: neither pcbnew nor eeschema artifacts found in output/ or docker volume" >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# wxWidgets WASM JavaScript glue code (defines JS functions called from WASM)
|
# wxWidgets WASM JavaScript glue code (defines JS functions called from WASM)
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,16 @@
|
||||||
# We provide stub values so CMake configuration succeeds
|
# We provide stub values so CMake configuration succeeds
|
||||||
|
|
||||||
if(EMSCRIPTEN OR NOT KICAD_SPICE)
|
if(EMSCRIPTEN OR NOT KICAD_SPICE)
|
||||||
message(STATUS "ngspice not available for WASM build (SPICE disabled)")
|
message(STATUS "ngspice not available for WASM build (using header stub)")
|
||||||
|
|
||||||
# Set variables to indicate ngspice is "found" but disabled
|
# Set variables to indicate ngspice is "found" but disabled
|
||||||
set(ngspice_FOUND TRUE)
|
set(ngspice_FOUND TRUE)
|
||||||
set(NGSPICE_FOUND TRUE)
|
set(NGSPICE_FOUND TRUE)
|
||||||
|
|
||||||
# Provide empty values
|
# Point at our header-only stub at wasm/stubs/ngspice/sharedspice.h so
|
||||||
set(NGSPICE_INCLUDE_DIR "")
|
# eeschema's sim/ngspice.{h,cpp} can compile. The library link line stays
|
||||||
|
# empty — the simulator frame is never instantiated in WASM.
|
||||||
|
set(NGSPICE_INCLUDE_DIR "${CMAKE_CURRENT_LIST_DIR}/../stubs")
|
||||||
set(NGSPICE_LIBRARY "")
|
set(NGSPICE_LIBRARY "")
|
||||||
set(NGSPICE_LIBRARIES "")
|
set(NGSPICE_LIBRARIES "")
|
||||||
|
|
||||||
|
|
|
||||||
111
wasm/stubs/char_traits_uint16_workaround.h
Normal file
111
wasm/stubs/char_traits_uint16_workaround.h
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
* libc++ workaround: provide std::char_traits<unsigned short> for WASM builds.
|
||||||
|
*
|
||||||
|
* KiCad's third-party Altium parser uses
|
||||||
|
* typedef std::basic_string<uint16_t> utf16string;
|
||||||
|
* (kicad/thirdparty/compoundfilereader/compoundfilereader.h:264).
|
||||||
|
*
|
||||||
|
* Modern libc++ (the version bundled with current Emscripten) pulls
|
||||||
|
* <__format/parser_std_format_spec.h> via <vector>, which triggers implicit
|
||||||
|
* instantiation of char_traits<unsigned short>. The standard only specializes
|
||||||
|
* char_traits for char / wchar_t / char8_t / char16_t / char32_t, so the
|
||||||
|
* uint16_t (== unsigned short) usage now fails to compile.
|
||||||
|
*
|
||||||
|
* We force-include this header into every translation unit via the build
|
||||||
|
* script's CMAKE_CXX_FLAGS so the specialization is visible before any code
|
||||||
|
* that needs it. Specializing std::char_traits for non-standard types is
|
||||||
|
* technically undefined per the standard but is the established workaround
|
||||||
|
* historically supported by libc++/libstdc++.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KICAD_WASM_CHAR_TRAITS_UINT16_WORKAROUND_H
|
||||||
|
#define KICAD_WASM_CHAR_TRAITS_UINT16_WORKAROUND_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cwchar>
|
||||||
|
#include <ios>
|
||||||
|
|
||||||
|
namespace std {
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct char_traits<unsigned short>
|
||||||
|
{
|
||||||
|
using char_type = unsigned short;
|
||||||
|
using int_type = int;
|
||||||
|
using off_type = streamoff;
|
||||||
|
using pos_type = fpos<mbstate_t>;
|
||||||
|
using state_type = mbstate_t;
|
||||||
|
|
||||||
|
static constexpr void assign( char_type& a, const char_type& b ) noexcept { a = b; }
|
||||||
|
static constexpr bool eq( char_type a, char_type b ) noexcept { return a == b; }
|
||||||
|
static constexpr bool lt( char_type a, char_type b ) noexcept { return a < b; }
|
||||||
|
|
||||||
|
static int compare( const char_type* s1, const char_type* s2, size_t n )
|
||||||
|
{
|
||||||
|
for( size_t i = 0; i < n; ++i )
|
||||||
|
{
|
||||||
|
if( s1[i] < s2[i] ) return -1;
|
||||||
|
if( s1[i] > s2[i] ) return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t length( const char_type* s )
|
||||||
|
{
|
||||||
|
size_t i = 0;
|
||||||
|
while( s[i] != 0 ) ++i;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char_type* find( const char_type* s, size_t n, const char_type& a )
|
||||||
|
{
|
||||||
|
for( size_t i = 0; i < n; ++i )
|
||||||
|
if( s[i] == a ) return s + i;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char_type* move( char_type* s1, const char_type* s2, size_t n )
|
||||||
|
{
|
||||||
|
return static_cast<char_type*>( memmove( s1, s2, n * sizeof( char_type ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static char_type* copy( char_type* s1, const char_type* s2, size_t n )
|
||||||
|
{
|
||||||
|
return static_cast<char_type*>( memcpy( s1, s2, n * sizeof( char_type ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static char_type* assign( char_type* s, size_t n, char_type a )
|
||||||
|
{
|
||||||
|
for( size_t i = 0; i < n; ++i ) s[i] = a;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr int_type not_eof( int_type c ) noexcept
|
||||||
|
{
|
||||||
|
return c == eof() ? static_cast<int_type>( 0 ) : c;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr char_type to_char_type( int_type c ) noexcept
|
||||||
|
{
|
||||||
|
return static_cast<char_type>( c );
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr int_type to_int_type( char_type c ) noexcept
|
||||||
|
{
|
||||||
|
return static_cast<int_type>( c );
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr bool eq_int_type( int_type a, int_type b ) noexcept { return a == b; }
|
||||||
|
static constexpr int_type eof() noexcept { return static_cast<int_type>( -1 ); }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace std
|
||||||
|
|
||||||
|
#endif // __EMSCRIPTEN__
|
||||||
|
#endif // __cplusplus
|
||||||
|
|
||||||
|
#endif // KICAD_WASM_CHAR_TRAITS_UINT16_WORKAROUND_H
|
||||||
15
wasm/stubs/eeschema_frame_stub.cpp
Normal file
15
wasm/stubs/eeschema_frame_stub.cpp
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Eeschema frame stubs for KiCad WASM build.
|
||||||
|
*
|
||||||
|
* Mirror of pcb_frame_stub.cpp. Populate as linker errors surface during the
|
||||||
|
* first eeschema-wasm build. Methods that need to be stubbed are typically:
|
||||||
|
* - Scripting helpers (LoadSchematic / SaveSchematic) when KICAD_SCRIPTING=OFF
|
||||||
|
* - Action-plugin glue (no plugins in WASM)
|
||||||
|
* - Filesystem-watcher hooks when wxUSE_FSWATCHER=0
|
||||||
|
*
|
||||||
|
* Leave this file empty until the linker complains; the build script skips
|
||||||
|
* compiling it when it has zero bytes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#endif
|
||||||
28
wasm/stubs/eeschema_ngspice_data_stubs.cpp
Normal file
28
wasm/stubs/eeschema_ngspice_data_stubs.cpp
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Empty replacements for the four largest ngspice model data initializers.
|
||||||
|
*
|
||||||
|
* Each of sim_model_ngspice_data_{bsim4,b3soi,b4soi,hsim}.cpp defines a
|
||||||
|
* single function (addBSIM4/addB3SOI/addB4SOI/addHSIM) that pushes hundreds
|
||||||
|
* of entries into NGSPICE_MODEL_INFO_MAP::modelInfos[...]. Once compiled to
|
||||||
|
* WASM these functions exceed the V8/SpiderMonkey limit on locals per
|
||||||
|
* function ("too many locals"), so Firefox refuses to instantiate the
|
||||||
|
* resulting module.
|
||||||
|
*
|
||||||
|
* The simulator UI is never reachable in the WASM build (FRAME_SIMULATOR
|
||||||
|
* fails to instantiate via the ngspice header stub at
|
||||||
|
* wasm/stubs/ngspice/sharedspice.h), so leaving these tables empty is safe.
|
||||||
|
*
|
||||||
|
* eeschema/CMakeLists.txt excludes the original four sources from
|
||||||
|
* EESCHEMA_SIM_SRCS for EMSCRIPTEN and adds this file instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
|
||||||
|
#include <sim/sim_model_ngspice.h>
|
||||||
|
|
||||||
|
void NGSPICE_MODEL_INFO_MAP::addBSIM4() {}
|
||||||
|
void NGSPICE_MODEL_INFO_MAP::addB3SOI() {}
|
||||||
|
void NGSPICE_MODEL_INFO_MAP::addB4SOI() {}
|
||||||
|
void NGSPICE_MODEL_INFO_MAP::addHSIM() {}
|
||||||
|
|
||||||
|
#endif // __EMSCRIPTEN__
|
||||||
55
wasm/stubs/ngspice/sharedspice.h
Normal file
55
wasm/stubs/ngspice/sharedspice.h
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Minimal stub of ngspice's sharedspice.h for KiCad WASM builds.
|
||||||
|
*
|
||||||
|
* Only the type names referenced by kicad/eeschema/sim/ngspice.{h,cpp} need
|
||||||
|
* to exist. The eeschema sim layer compiles but the simulator frame is never
|
||||||
|
* instantiated in WASM (FRAME_SIMULATOR's try/catch in IFACE::CreateKiWindow
|
||||||
|
* catches the init failure and returns nullptr).
|
||||||
|
*
|
||||||
|
* We intentionally do NOT define NGSPICE_PACKAGE_VERSION so that ngspice.h's
|
||||||
|
* fallback `typedef bool NG_BOOL;` (line 46) provides the boolean type.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KICAD_WASM_NGSPICE_SHAREDSPICE_STUB_H
|
||||||
|
#define KICAD_WASM_NGSPICE_SHAREDSPICE_STUB_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct ngcomplex {
|
||||||
|
double cx_real;
|
||||||
|
double cx_imag;
|
||||||
|
} ngcomplex_t;
|
||||||
|
|
||||||
|
struct vector_info {
|
||||||
|
char* v_name;
|
||||||
|
int v_type;
|
||||||
|
short v_flags;
|
||||||
|
double* v_realdata;
|
||||||
|
ngcomplex_t* v_compdata;
|
||||||
|
int v_length;
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct vector_info* pvector_info;
|
||||||
|
|
||||||
|
/* Opaque payload types for callbacks we never wire up (SendData/SendInitData). */
|
||||||
|
typedef struct vecvaluesall* pvecvaluesall;
|
||||||
|
typedef struct vecinfoall* pvecinfoall;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function types (not pointers). ngspice.h references them as `SendChar*` etc.,
|
||||||
|
* so the trailing star in the typedef site makes the pointer.
|
||||||
|
*/
|
||||||
|
typedef int (SendChar)(char*, int, void*);
|
||||||
|
typedef int (SendStat)(char*, int, void*);
|
||||||
|
typedef int (ControlledExit)(int, bool, bool, int, void*);
|
||||||
|
typedef int (SendData)(pvecvaluesall, int, int, void*);
|
||||||
|
typedef int (SendInitData)(pvecinfoall, int, void*);
|
||||||
|
typedef int (BGThreadRunning)(bool, int, void*);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* KICAD_WASM_NGSPICE_SHAREDSPICE_STUB_H */
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 6fb2eac2572cf0d3964ba8bec8d73e017d311733
|
Subproject commit d1d1627b279672fc71deb4ff4512a7771dfc2cc8
|
||||||
Loading…
Reference in a new issue