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
|
||||
# Build KiCad WASM inside Docker container, then apply asyncify on host
|
||||
|
||||
# Redirect all output to a log file (re-execs script with redirection)
|
||||
source "$(dirname "$0")/../scripts/common/logging.sh"
|
||||
# Build a KiCad editor (pcbnew or eeschema) inside Docker, then run asyncify
|
||||
# and friends on the host.
|
||||
#
|
||||
# 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:
|
||||
# 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.
|
||||
|
||||
# Redirect all output to a log file (re-execs script with redirection)
|
||||
source "$(dirname "$0")/../scripts/common/logging.sh"
|
||||
|
||||
set -e
|
||||
|
||||
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
|
||||
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD | tr '/' '-' | tr '[:upper:]' '[:lower:]')
|
||||
export COMPOSE_PROJECT_NAME="kicad-wasm-${BRANCH_NAME}"
|
||||
echo "Using Docker project: ${COMPOSE_PROJECT_NAME}"
|
||||
echo "Building app: ${APP_NAME}"
|
||||
|
||||
# Add -j 10 by default if no -j flag is given
|
||||
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
|
||||
fi
|
||||
|
||||
# Run build command (without asyncify - handled on host due to memory requirements)
|
||||
# -e EMSDK=/emsdk: `docker compose exec` bypasses the entrypoint that sources
|
||||
# Build one app: compile in container, then run host-side post-processing.
|
||||
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 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 \
|
||||
/workspace/scripts/kicad/build-pcbnew.sh "${ARGS[@]}"
|
||||
docker compose -f docker/docker-compose.yml exec -e EMSDK=/emsdkkicad-wasm-builder \
|
||||
"/workspace/scripts/kicad/build-${app}.sh" "${ARGS[@]}"
|
||||
|
||||
# Copy output to host-accessible directory
|
||||
# Note: pcbnew.wasm.debug.wasm contains DWARF debug info (generated with -gseparate-dwarf)
|
||||
echo "Copying build output to ./output/..."
|
||||
docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \
|
||||
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-pcbnew/pcbnew/pcbnew.{js,wasm} /workspace/output/; \
|
||||
cp /workspace/build-wasm/kicad-pcbnew/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"
|
||||
# Copy output to host-accessible directory.
|
||||
# ${app}.wasm.debug.wasm contains DWARF debug info (when built with -gseparate-dwarf).
|
||||
echo "Copying ${app} build output to ./output/..."
|
||||
docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \
|
||||
bash -c "mkdir -p /workspace/output && \
|
||||
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-${app}/${app}/${app}.{js,wasm} /workspace/output/; \
|
||||
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"
|
||||
|
||||
# Inject dynCall shims into pcbnew.js
|
||||
# This fixes "dynCall_* is not defined" errors in Emscripten 4.x
|
||||
./scripts/common/inject-dyncall-shims.sh output/pcbnew.js
|
||||
# Inject dynCall shims (fixes "dynCall_* is not defined" errors in Emscripten 4.x)
|
||||
./scripts/common/inject-dyncall-shims.sh "output/${app}.js"
|
||||
|
||||
# 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/pcbnew.wasm output/pcbnew.wasm
|
||||
# Apply wasm-emscripten-finalize on host (skipped in Docker due to memory limits)
|
||||
./scripts/common/apply-finalize.sh "output/${app}.wasm" "output/${app}.wasm"
|
||||
|
||||
# Apply asyncify transformation on host
|
||||
# This is done on the host because wasm-opt --asyncify needs significant RAM
|
||||
./scripts/common/apply-asyncify.sh output/pcbnew.wasm output/pcbnew.wasm
|
||||
# Apply asyncify transformation on host
|
||||
./scripts/common/apply-asyncify.sh "output/${app}.wasm" "output/${app}.wasm"
|
||||
}
|
||||
|
||||
if [[ "${APP_NAME}" == "all" ]]; then
|
||||
build_app pcbnew
|
||||
build_app eeschema
|
||||
else
|
||||
build_app "${APP_NAME}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
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"
|
||||
|
||||
# Root repo patch (exclude submodules)
|
||||
git diff HEAD -- ':!kicad' ':!wxwidgets' > "$FEATURE_DIR/root.patch"
|
||||
# Root repo patch (exclude submodules and features/ — the latter would cause
|
||||
# the patch to contain itself recursively).
|
||||
git diff HEAD -- ':!kicad' ':!wxwidgets' ':!features' > "$FEATURE_DIR/root.patch"
|
||||
|
||||
# Submodule patches (diff from upstream base)
|
||||
KICAD_BASE=$(git -C kicad log --format='%H' --author-not='viktor.vaczi@emergence-engineering.com' --author-not='noreply@anthropic.com' -1)
|
||||
git -C kicad diff $KICAD_BASE > "$FEATURE_DIR/kicad.patch"
|
||||
# Submodule patches: diff against main's recorded submodule sha so the patch
|
||||
# captures only this feature branch's submodule work (committed + uncommitted),
|
||||
# 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"
|
||||
git -C wxwidgets diff $WX_BASE > "$FEATURE_DIR/wxwidgets.patch"
|
||||
sub_diff kicad "$FEATURE_DIR/kicad.patch"
|
||||
sub_diff wxwidgets "$FEATURE_DIR/wxwidgets.patch"
|
||||
|
||||
echo "Patches created in $FEATURE_DIR/"
|
||||
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
|
||||
# Build KiCad PCBnew for WebAssembly
|
||||
# This builds the PCB editor as a standalone WASM application
|
||||
#
|
||||
# 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
|
||||
# Build KiCad PCBnew for WebAssembly.
|
||||
# Thin wrapper around build-kicad-target.sh — see that script for options.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/../common/env.sh"
|
||||
source "${SCRIPT_DIR}/../common/versions.sh"
|
||||
source "${SCRIPT_DIR}/../common/functions.sh"
|
||||
|
||||
KICAD_DIR="${PROJECT_ROOT}/kicad"
|
||||
KICAD_BUILD="${BUILD_ROOT}/kicad-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"
|
||||
exec "${SCRIPT_DIR}/build-kicad-target.sh" pcbnew "$@"
|
||||
|
|
|
|||
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",
|
||||
"serve": "npx serve apps -p 8080 -c ../serve.json",
|
||||
"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:chrome": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed",
|
||||
"test:kicad": "npm run test:kicad:firefox",
|
||||
"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:chrome": "npm run setup:calculator && playwright test --config=playwright-calculator.config.ts --project=chromium --headed",
|
||||
"test:calculator": "npm run test:calculator:firefox",
|
||||
"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: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)
|
||||
# Fallback: Copy from Docker volume directly
|
||||
#
|
||||
# Copies whichever editors are present (pcbnew, eeschema).
|
||||
|
||||
set -e
|
||||
|
||||
|
|
@ -13,32 +15,46 @@ OUTPUT_DIR="$PROJECT_ROOT/output"
|
|||
|
||||
mkdir -p "$KICAD_TEST"
|
||||
|
||||
# Check if output directory has the build files
|
||||
if [ -f "$OUTPUT_DIR/pcbnew.js" ] && [ -f "$OUTPUT_DIR/pcbnew.wasm" ]; then
|
||||
echo "Copying KiCad WASM files from output directory..."
|
||||
cp "$OUTPUT_DIR/pcbnew.js" "$KICAD_TEST/"
|
||||
cp "$OUTPUT_DIR/pcbnew.wasm" "$KICAD_TEST/"
|
||||
# Source map for debug symbols (optional)
|
||||
cp "$OUTPUT_DIR/pcbnew.wasm.map" "$KICAD_TEST/" 2>/dev/null || true
|
||||
# Worker file for pthreads (optional)
|
||||
cp "$OUTPUT_DIR/pcbnew.worker.js" "$KICAD_TEST/" 2>/dev/null || true
|
||||
# Bitmap resources for KiCad icons (optional)
|
||||
cp "$OUTPUT_DIR/images.tar.gz" "$KICAD_TEST/" 2>/dev/null || true
|
||||
else
|
||||
echo "Output directory not found, copying from Docker build..."
|
||||
docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
||||
kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.js "$KICAD_TEST/"
|
||||
docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
||||
kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.wasm "$KICAD_TEST/"
|
||||
# Source map for debug symbols (optional)
|
||||
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
|
||||
# Worker file for pthreads (optional)
|
||||
docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
||||
kicad-wasm-builder:/workspace/build-wasm/kicad-pcbnew/pcbnew/pcbnew.worker.js "$KICAD_TEST/" 2>/dev/null || true
|
||||
# Bitmap resources for KiCad icons (optional)
|
||||
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
|
||||
# Copy one editor's artifacts (js, wasm, optional debug/map/worker). Returns 0
|
||||
# if the editor was present, 1 if neither output/ nor the docker volume has it.
|
||||
copy_app() {
|
||||
local app="$1"
|
||||
|
||||
if [ -f "$OUTPUT_DIR/${app}.js" ] && [ -f "$OUTPUT_DIR/${app}.wasm" ]; then
|
||||
echo "Copying ${app} WASM files from output directory..."
|
||||
cp "$OUTPUT_DIR/${app}.js" "$KICAD_TEST/"
|
||||
cp "$OUTPUT_DIR/${app}.wasm" "$KICAD_TEST/"
|
||||
cp "$OUTPUT_DIR/${app}.wasm.map" "$KICAD_TEST/" 2>/dev/null || true
|
||||
cp "$OUTPUT_DIR/${app}.worker.js" "$KICAD_TEST/" 2>/dev/null || true
|
||||
cp "$OUTPUT_DIR/images.tar.gz" "$KICAD_TEST/" 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Output ${app} not found locally, trying Docker volume..."
|
||||
if docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
||||
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 \
|
||||
kicad-wasm-builder:/workspace/build-wasm/kicad-${app}/${app}/${app}.wasm "$KICAD_TEST/" 2>/dev/null; then
|
||||
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
|
||||
docker compose -f "$PROJECT_ROOT/docker/docker-compose.yml" cp \
|
||||
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 \
|
||||
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
|
||||
|
||||
# wxWidgets WASM JavaScript glue code (defines JS functions called from WASM)
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@
|
|||
# We provide stub values so CMake configuration succeeds
|
||||
|
||||
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(ngspice_FOUND TRUE)
|
||||
set(NGSPICE_FOUND TRUE)
|
||||
|
||||
# Provide empty values
|
||||
set(NGSPICE_INCLUDE_DIR "")
|
||||
# Point at our header-only stub at wasm/stubs/ngspice/sharedspice.h so
|
||||
# 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_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