feat: kicad_tools headless CLI
One merged node-WASM CLI (wasm/tools, 26.5MB) supersedes sym_convert + pcb_convert: --convert-lib (absolutizes paths — fixes the legacy plugin's silent empty output on relative paths), --lint (now full-parses .kicad_pcb via the linked pcbnew parser), --erc, --netlist, --bom, --plot, --drc. Registered as the only headless CLI app; ASYNCIFY=0 CLIs skip the nanosleep→Asyncify yield shim and exit via _exit (dieted static dtors trap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADzCSeN3Q9DJL3YW2FZTXB
This commit is contained in:
parent
396d4c1d45
commit
ad9c4675f9
12 changed files with 1304 additions and 86 deletions
|
|
@ -81,7 +81,7 @@ trap 'kw_fail 130; exit 130' INT TERM
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
VALID_APPS="kicad_editor | pcbnew | eeschema | calculator | pl_editor | gerbview | sym_convert | occ_service | all"
|
VALID_APPS="kicad_editor | pcbnew | eeschema | calculator | pl_editor | gerbview | kicad_tools | occ_service | all"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: ./docker/build.sh <app>[,<app>...] [args...]" >&2
|
echo "Usage: ./docker/build.sh <app>[,<app>...] [args...]" >&2
|
||||||
|
|
@ -114,7 +114,7 @@ else
|
||||||
IFS=',' read -r -a APPS <<< "$APP_NAME"
|
IFS=',' read -r -a APPS <<< "$APP_NAME"
|
||||||
for app in "${APPS[@]}"; do
|
for app in "${APPS[@]}"; do
|
||||||
case "$app" in
|
case "$app" in
|
||||||
kicad_editor|pcbnew|eeschema|calculator|pl_editor|gerbview|sym_convert|occ_service) ;;
|
kicad_editor|pcbnew|eeschema|calculator|pl_editor|gerbview|kicad_tools|occ_service) ;;
|
||||||
*)
|
*)
|
||||||
echo "Error: unknown app '$app' (expected: ${VALID_APPS})" >&2
|
echo "Error: unknown app '$app' (expected: ${VALID_APPS})" >&2
|
||||||
usage
|
usage
|
||||||
|
|
@ -213,7 +213,6 @@ kicad_subdir_for() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
calculator) echo "pcb_calculator" ;;
|
calculator) echo "pcb_calculator" ;;
|
||||||
pl_editor) echo "pagelayout_editor" ;;
|
pl_editor) echo "pagelayout_editor" ;;
|
||||||
sym_convert) echo "eeschema" ;;
|
|
||||||
*) echo "$1" ;;
|
*) echo "$1" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +238,7 @@ compile_app() {
|
||||||
# EMSDK lets scripts/common/env.sh source /emsdk/emsdk_env.sh and activate the toolchain.
|
# EMSDK lets scripts/common/env.sh source /emsdk/emsdk_env.sh and activate the toolchain.
|
||||||
# BUILD_3D_VIEWER passes through EMPTY when the host didn't set it, so
|
# BUILD_3D_VIEWER passes through EMPTY when the host didn't set it, so
|
||||||
# build-kicad-target.sh can apply per-app defaults (ON for editors, OFF
|
# build-kicad-target.sh can apply per-app defaults (ON for editors, OFF
|
||||||
# for headless CLIs like sym_convert — the gl1 shim needs glm).
|
# for headless CLIs like kicad_tools — the gl1 shim needs glm).
|
||||||
docker compose -f docker/docker-compose.yml exec -e EMSDK=/emsdk \
|
docker compose -f docker/docker-compose.yml exec -e EMSDK=/emsdk \
|
||||||
-e BUILD_3D_VIEWER="${BUILD_3D_VIEWER:-}" \
|
-e BUILD_3D_VIEWER="${BUILD_3D_VIEWER:-}" \
|
||||||
kicad-wasm-builder \
|
kicad-wasm-builder \
|
||||||
|
|
@ -273,10 +272,10 @@ postprocess_app() {
|
||||||
local app="$1"
|
local app="$1"
|
||||||
local out_dir="output"
|
local out_dir="output"
|
||||||
|
|
||||||
# The converter and the OCC service are finalized in-container (real tools,
|
# The headless CLI and the OCC service are finalized in-container (real
|
||||||
# small -g0 wasm) and build with ASYNCIFY=0, so they need no host
|
# tools, small -g0 wasm) and build with ASYNCIFY=0, so they need no host
|
||||||
# post-processing (no dyncall shims, no finalize, no asyncify).
|
# post-processing (no dyncall shims, no finalize, no asyncify).
|
||||||
if [ "$app" = "sym_convert" ] || [ "$app" = "occ_service" ]; then
|
if [ "$app" = "kicad_tools" ] || [ "$app" = "occ_service" ]; then
|
||||||
echo "Skipping host post-processing for ${app} (finalized in-container)"
|
echo "Skipping host post-processing for ${app} (finalized in-container)"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
@ -296,14 +295,12 @@ postprocess_app() {
|
||||||
kw_stage finalize
|
kw_stage finalize
|
||||||
./scripts/common/apply-finalize.sh "${out_dir}/${app}.wasm" "${out_dir}/${app}.wasm"
|
./scripts/common/apply-finalize.sh "${out_dir}/${app}.wasm" "${out_dir}/${app}.wasm"
|
||||||
|
|
||||||
# Apply asyncify transformation on host. The converter is a synchronous node
|
# Apply asyncify transformation on host (the ASYNCIFY=0 CLIs returned
|
||||||
# CLI built with ASYNCIFY=0, so asyncify is unnecessary and would be wrong.
|
# early above and never reach this).
|
||||||
# apply-asyncify always runs the --hoist-cpp-catches pass FIRST (native wasm-EH is the only build
|
# apply-asyncify always runs the --hoist-cpp-catches pass FIRST (native wasm-EH is the only build
|
||||||
# mode) so Asyncify can suspend from inside C++ catch arms, then asyncify + removelist + wasm-opt -O1.
|
# mode) so Asyncify can suspend from inside C++ catch arms, then asyncify + removelist + wasm-opt -O1.
|
||||||
if [ "$app" != "sym_convert" ]; then
|
kw_stage asyncify
|
||||||
kw_stage asyncify
|
./scripts/common/apply-asyncify.sh "${out_dir}/${app}.wasm" "${out_dir}/${app}.wasm"
|
||||||
./scripts/common/apply-asyncify.sh "${out_dir}/${app}.wasm" "${out_dir}/${app}.wasm"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Pipelined driver state (KICAD_PIPELINE=1) ---
|
# --- Pipelined driver state (KICAD_PIPELINE=1) ---
|
||||||
|
|
|
||||||
2
kicad
2
kicad
|
|
@ -1 +1 @@
|
||||||
Subproject commit ca19e5d6c85688937e08bcd4989b8c93e07be382
|
Subproject commit acaf47234afe2e93400b23a93b6401b28d9932db
|
||||||
|
|
@ -66,15 +66,18 @@ case "$APP_NAME" in
|
||||||
KICAD_TARGET="pcb_calculator"
|
KICAD_TARGET="pcb_calculator"
|
||||||
KICAD_SUBDIR="pcb_calculator"
|
KICAD_SUBDIR="pcb_calculator"
|
||||||
;;
|
;;
|
||||||
sym_convert)
|
kicad_tools)
|
||||||
# Standalone .lib -> .kicad_sym converter + --lint CLI (node). Its
|
# Merged headless CLI (pcbjam-mcp 0001 tier 3a): both dieted kifaces —
|
||||||
# add_executable lives in eeschema/CMakeLists.txt (gated by
|
# .lib conversion (--convert-lib), --lint, --erc, --netlist, --bom,
|
||||||
# KICAD_SYM_CONVERTER_WASM), so artifacts land in the eeschema/ subdir
|
# --plot, --drc in one node image. (Supersedes the retired standalone
|
||||||
# of its own kicad-sym_convert tree. A headless CLI has no 3D viewer:
|
# sym_convert / pcb_convert apps.) Its add_executable lives in
|
||||||
# skip the wasm/gl1 FFP shim compile (it needs glm, which the libs
|
# wasm/tools/ (added by the fork's top-level CMakeLists under
|
||||||
# sysroot doesn't guarantee) unless the caller forces it.
|
# -DKICAD_TOOLS_WASM=ON); the binary dir doubles as the artifact
|
||||||
KICAD_TARGET="sym_convert"
|
# subdir of its own kicad-kicad_tools tree. A headless CLI has no 3D
|
||||||
KICAD_SUBDIR="eeschema"
|
# viewer: skip the wasm/gl1 FFP shim compile (it needs glm, which the
|
||||||
|
# libs sysroot doesn't guarantee) unless the caller forces it.
|
||||||
|
KICAD_TARGET="kicad_tools"
|
||||||
|
KICAD_SUBDIR="kicad_tools"
|
||||||
BUILD_3D_VIEWER="${BUILD_3D_VIEWER:-OFF}"
|
BUILD_3D_VIEWER="${BUILD_3D_VIEWER:-OFF}"
|
||||||
;;
|
;;
|
||||||
occ_service)
|
occ_service)
|
||||||
|
|
@ -86,17 +89,17 @@ case "$APP_NAME" in
|
||||||
KICAD_SUBDIR="occ_service"
|
KICAD_SUBDIR="occ_service"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Error: unknown app '$APP_NAME' (expected: kicad_editor | pcbnew | eeschema | calculator | pl_editor | gerbview | sym_convert | occ_service)" >&2
|
echo "Error: unknown app '$APP_NAME' (expected: kicad_editor | pcbnew | eeschema | calculator | pl_editor | gerbview | kicad_tools | occ_service)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Which app's embind bindings to compile + link. Most apps use their own.
|
# Which app's embind bindings to compile + link. Most apps use their own.
|
||||||
# NOTE sym_convert deliberately does NOT reuse eeschema's embind object even
|
# NOTE kicad_tools deliberately does NOT reuse the editors' embind objects even
|
||||||
# though it links the eeschema kiface: the kiface references exactly one embind
|
# though it links both kifaces: each kiface references exactly one embind
|
||||||
# symbol (kicadCollabOnSave), and linking the real bindings TU for it roots the
|
# symbol (kicadCollabOnSave), and linking a real bindings TU for it roots the
|
||||||
# whole editor surface from .init_array (~4x binary size — ysync 0009 size
|
# whole editor surface from .init_array (~4x binary size — ysync 0009 size
|
||||||
# research). Its own wasm/bindings/sym_convert_embind.cpp provides a no-op hook.
|
# research). Its own wasm/bindings/kicad_tools_embind.cpp provides a no-op hook.
|
||||||
case "$APP_NAME" in
|
case "$APP_NAME" in
|
||||||
# occ_service links the pcbnew kiface objects → pcbnew's embind object (its
|
# occ_service links the pcbnew kiface objects → pcbnew's embind object (its
|
||||||
# own embind entry points live in occ_service_main.cpp, compiled inside the
|
# own embind entry points live in occ_service_main.cpp, compiled inside the
|
||||||
|
|
@ -105,20 +108,21 @@ case "$APP_NAME" in
|
||||||
*) EMBIND_APP="$APP_NAME" ;;
|
*) EMBIND_APP="$APP_NAME" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Embind linker support (--bind). sym_convert has no bindings at all (see
|
# Embind linker support (--bind). kicad_tools has no bindings at all (see
|
||||||
# above) — dropping --bind keeps the embind JS/native runtime out entirely.
|
# above) — dropping --bind keeps the embind JS/native runtime out entirely.
|
||||||
case "$APP_NAME" in
|
case "$APP_NAME" in
|
||||||
sym_convert) EMBIND_LINK_FLAG="" ;;
|
kicad_tools) EMBIND_LINK_FLAG="" ;;
|
||||||
*) EMBIND_LINK_FLAG="--bind" ;;
|
*) EMBIND_LINK_FLAG="--bind" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Which app's WASM stub libraries (scripting/frame placeholders) to link.
|
# Which app's WASM stub libraries (scripting/frame placeholders) to link.
|
||||||
# sym_convert links the eeschema kiface objects, so it needs eeschema's frame
|
# kicad_editor links pcbnew's kiface objects, which reference the action-plugin
|
||||||
# stub (eeschema_frame_stub.cpp). kicad_editor links pcbnew's kiface objects, which
|
# scripting placeholders (pcbnewGet*); eeschema's frame stub arrives via CMake
|
||||||
# reference the action-plugin scripting placeholders (pcbnewGet*); eeschema's frame
|
# (target_sources on eeschema_kiface_objects), not this path.
|
||||||
# stub arrives via CMake (target_sources on eeschema_kiface_objects), not this path.
|
|
||||||
case "$APP_NAME" in
|
case "$APP_NAME" in
|
||||||
sym_convert) STUB_APP="eeschema" ;;
|
# kicad_tools links both kifaces: eeschema's frame stub comes through this
|
||||||
|
# path, pcbnew's via CMake PCBNEW_WASM_STUBS.
|
||||||
|
kicad_tools) STUB_APP="eeschema" ;;
|
||||||
kicad_editor) STUB_APP="pcbnew" ;;
|
kicad_editor) STUB_APP="pcbnew" ;;
|
||||||
# occ_service links the pcbnew kiface objects → pcbnew's stubs (frame +
|
# occ_service links the pcbnew kiface objects → pcbnew's stubs (frame +
|
||||||
# action-plugin scripting placeholders), like the editors.
|
# action-plugin scripting placeholders), like the editors.
|
||||||
|
|
@ -356,7 +360,7 @@ log_info "Stub libraries built"
|
||||||
# Step 6.2/6.3: wasm-opt + wasm-emscripten-finalize handling.
|
# Step 6.2/6.3: wasm-opt + wasm-emscripten-finalize handling.
|
||||||
# For the editor apps these tools OOM on the huge debug wasm, so we stub them in
|
# For the editor apps these tools OOM on the huge debug wasm, so we stub them in
|
||||||
# the container and run them on the host (docker/build.sh phase 2). The small,
|
# the container and run them on the host (docker/build.sh phase 2). The small,
|
||||||
# debug-stripped (-g0) converter finalizes fine in-container, so for sym_convert
|
# debug-stripped (-g0) CLI finalizes fine in-container, so for kicad_tools
|
||||||
# we restore/keep the real tools and skip host post-processing entirely.
|
# we restore/keep the real tools and skip host post-processing entirely.
|
||||||
if [ -z "${EMSDK}" ]; then
|
if [ -z "${EMSDK}" ]; then
|
||||||
log_error "EMSDK environment variable is not set."
|
log_error "EMSDK environment variable is not set."
|
||||||
|
|
@ -365,7 +369,7 @@ fi
|
||||||
EMSDK_WASM_OPT="${EMSDK}/upstream/bin/wasm-opt"
|
EMSDK_WASM_OPT="${EMSDK}/upstream/bin/wasm-opt"
|
||||||
EMSDK_FINALIZE="${EMSDK}/upstream/bin/wasm-emscripten-finalize"
|
EMSDK_FINALIZE="${EMSDK}/upstream/bin/wasm-emscripten-finalize"
|
||||||
|
|
||||||
if [ "${APP_NAME}" = "sym_convert" ] || [ "${APP_NAME}" = "occ_service" ]; then
|
if [ "${APP_NAME}" = "kicad_tools" ] || [ "${APP_NAME}" = "occ_service" ]; then
|
||||||
# Use the real tools so the small -g0 module is fully finalized inside the
|
# Use the real tools so the small -g0 module is fully finalized inside the
|
||||||
# container (no host post-processing / asyncify for these targets).
|
# container (no host post-processing / asyncify for these targets).
|
||||||
[ -f "${EMSDK_WASM_OPT}.real" ] && cp "${EMSDK_WASM_OPT}.real" "${EMSDK_WASM_OPT}"
|
[ -f "${EMSDK_WASM_OPT}.real" ] && cp "${EMSDK_WASM_OPT}.real" "${EMSDK_WASM_OPT}"
|
||||||
|
|
@ -434,11 +438,12 @@ if command -v ccache &> /dev/null; then
|
||||||
log_info "Using ccache for compilation"
|
log_info "Using ccache for compilation"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The standalone converter is a gated eeschema target; enabling the option also
|
# The merged headless CLI configures with BOTH diet options (each trims its
|
||||||
# trims the SCH_IO factory to the two KiCad plugins (no pcbjam/http) for this tree.
|
# own kiface for this tree; the eeschema one also trims the SCH_IO factory to
|
||||||
SYM_CONVERTER_CMAKE_FLAG=""
|
# the two KiCad plugins) plus the wasm/tools/ subdir gate.
|
||||||
if [ "${APP_NAME}" = "sym_convert" ]; then
|
KICAD_TOOLS_CMAKE_FLAG=""
|
||||||
SYM_CONVERTER_CMAKE_FLAG="-DKICAD_SYM_CONVERTER_WASM=ON"
|
if [ "${APP_NAME}" = "kicad_tools" ]; then
|
||||||
|
KICAD_TOOLS_CMAKE_FLAG="-DKICAD_TOOLS_WASM=ON -DKICAD_SYM_CONVERTER_WASM=ON -DKICAD_PCB_CONVERTER_WASM=ON"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Merged pcbnew+eeschema editor: gates the wasm/editor/ subdir, the per-engine
|
# Merged pcbnew+eeschema editor: gates the wasm/editor/ subdir, the per-engine
|
||||||
|
|
@ -490,8 +495,17 @@ fi
|
||||||
# Mirrors the wasm/gl1 pattern (compile to .o, add to the link). Shim:
|
# Mirrors the wasm/gl1 pattern (compile to .o, add to the link). Shim:
|
||||||
# wasm/shims/nanosleep_yield.c; its EM_ASYNC_JS yield is covered by env.__asyncjs__* in
|
# wasm/shims/nanosleep_yield.c; its EM_ASYNC_JS yield is covered by env.__asyncjs__* in
|
||||||
# scripts/common/asyncify-imports.txt.
|
# scripts/common/asyncify-imports.txt.
|
||||||
emcc -c -pthread "${PROJECT_ROOT}/wasm/shims/nanosleep_yield.c" -o "${STUBS_BUILD}/nanosleep_yield.o"
|
# The synchronous node CLIs (ASYNCIFY=0) must NOT link it: their Asyncify JS
|
||||||
NANOSLEEP_YIELD_LINK="${STUBS_BUILD}/nanosleep_yield.o"
|
# runtime doesn't exist, so the shim's yield throws "Asyncify is not defined"
|
||||||
|
# on the first main-thread sleep (e.g. DRC copper-clearance's worker-poll).
|
||||||
|
# A blocking CLI wants libc's real blocking nanosleep anyway — node permits
|
||||||
|
# Atomics.wait on its main thread.
|
||||||
|
if [ "${APP_NAME}" = "kicad_tools" ] || [ "${APP_NAME}" = "occ_service" ]; then
|
||||||
|
NANOSLEEP_YIELD_LINK=""
|
||||||
|
else
|
||||||
|
emcc -c -pthread "${PROJECT_ROOT}/wasm/shims/nanosleep_yield.c" -o "${STUBS_BUILD}/nanosleep_yield.o"
|
||||||
|
NANOSLEEP_YIELD_LINK="${STUBS_BUILD}/nanosleep_yield.o"
|
||||||
|
fi
|
||||||
|
|
||||||
# mallinfo() stub for the mimalloc build: -sMALLOC=mimalloc doesn't export the
|
# mallinfo() stub for the mimalloc build: -sMALLOC=mimalloc doesn't export the
|
||||||
# glibc mallinfo() that OpenCASCADE's OSD_MemInfo.cxx (libTKernel, pcbnew's 3D)
|
# glibc mallinfo() that OpenCASCADE's OSD_MemInfo.cxx (libTKernel, pcbnew's 3D)
|
||||||
|
|
@ -518,7 +532,7 @@ fi
|
||||||
|
|
||||||
emcmake cmake "${KICAD_DIR}" \
|
emcmake cmake "${KICAD_DIR}" \
|
||||||
${CCACHE_OPTS} \
|
${CCACHE_OPTS} \
|
||||||
${SYM_CONVERTER_CMAKE_FLAG} \
|
${KICAD_TOOLS_CMAKE_FLAG} \
|
||||||
${MERGED_EDITOR_CMAKE_FLAG} \
|
${MERGED_EDITOR_CMAKE_FLAG} \
|
||||||
${OCC_SERVICE_CMAKE_FLAG} \
|
${OCC_SERVICE_CMAKE_FLAG} \
|
||||||
-DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
|
-DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
|
||||||
|
|
@ -672,7 +686,7 @@ emmake make -j${JOBS} "${KICAD_TARGET}"
|
||||||
# Step 8.1: Build bitmap resources (images.tar.gz)
|
# Step 8.1: Build bitmap resources (images.tar.gz)
|
||||||
# This creates the icon archive that KiCad loads at runtime. The headless
|
# This creates the icon archive that KiCad loads at runtime. The headless
|
||||||
# converter/service targets have no GUI/icons, so skip it.
|
# converter/service targets have no GUI/icons, so skip it.
|
||||||
if [ "${APP_NAME}" != "sym_convert" ] && [ "${APP_NAME}" != "occ_service" ]; then
|
if [ "${APP_NAME}" != "kicad_tools" ] && [ "${APP_NAME}" != "occ_service" ]; then
|
||||||
kw_stage kicad-bitmaps
|
kw_stage kicad-bitmaps
|
||||||
log_info "Building bitmap resources..."
|
log_info "Building bitmap resources..."
|
||||||
emmake make bitmap_archive_build
|
emmake make bitmap_archive_build
|
||||||
|
|
|
||||||
7
scripts/kicad/build-kicad_tools.sh
Executable file
7
scripts/kicad/build-kicad_tools.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Build the merged headless KiCad CLI (kicad_tools: sym_convert + pcb_convert
|
||||||
|
# subcommands in one node WASM image). 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" kicad_tools "$@"
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# Build the standalone .lib -> .kicad_sym converter (sym_convert) for WebAssembly,
|
|
||||||
# as a node CLI. 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" sym_convert "$@"
|
|
||||||
9
wasm/bindings/kicad_tools_embind.cpp
Normal file
9
wasm/bindings/kicad_tools_embind.cpp
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// kicad_tools' "embind" object — deliberately embind-free, mirroring
|
||||||
|
// sym_convert_embind.cpp / pcb_convert_embind.cpp (ysync 0009 size research).
|
||||||
|
//
|
||||||
|
// Both dieted kifaces reference exactly ONE editor-embind symbol:
|
||||||
|
// kicadCollabOnSave (eeschema files-io.cpp / pcbnew files.cpp save paths —
|
||||||
|
// neither executes headless, and both TUs are pruned from the diets anyway).
|
||||||
|
// This no-op hook satisfies any remaining reference without rooting the
|
||||||
|
// editor surface.
|
||||||
|
extern "C" void kicadCollabOnSave( const char* /* aPath */ ) {}
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
// sym_convert's "embind" object — deliberately embind-free (ysync 0009 size
|
|
||||||
// research, step (a)).
|
|
||||||
//
|
|
||||||
// The eeschema kiface references exactly ONE symbol from the editor's embind
|
|
||||||
// TU: kicadCollabOnSave (files-io.cpp, called from SCH_EDIT_FRAME::SaveEEFile —
|
|
||||||
// a path the headless converter never executes). Linking the real
|
|
||||||
// eeschema_embind.o for that one symbol roots the ENTIRE editor surface from
|
|
||||||
// .init_array (EMSCRIPTEN_BINDINGS takes the address of every bound function:
|
|
||||||
// kicadOpenFile → frames/tools/dialogs, collab bridge, presence → GAL), which
|
|
||||||
// GC cannot remove. This no-op hook satisfies the reference instead.
|
|
||||||
extern "C" void kicadCollabOnSave( const char* /* aPath */ ) {}
|
|
||||||
50
wasm/cli/kicad_tools_main.cpp
Normal file
50
wasm/cli/kicad_tools_main.cpp
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* kicad_tools — merged headless KiCad CLI, built as ONE WebAssembly module
|
||||||
|
* linking both dieted kifaces (pcbjam-mcp 0001 tier 3a).
|
||||||
|
*
|
||||||
|
* Thin dispatcher over the two per-app entry points (compiled into this image
|
||||||
|
* with KICAD_TOOLS_COMBINED, which strips their standalone main()s):
|
||||||
|
*
|
||||||
|
* pcbnew side (pcb_convert_main.cpp):
|
||||||
|
* kicad_tools --drc [--json] [--strict] <file.kicad_pcb> [<out>]
|
||||||
|
*
|
||||||
|
* eeschema side (sym_convert_main.cpp) — everything else:
|
||||||
|
* kicad_tools --convert-lib <input.lib> <output.kicad_sym>
|
||||||
|
* kicad_tools --lint [--strict] <file> [<file>...]
|
||||||
|
* (.kicad_pcb files get a FULL parse here — the pcbnew
|
||||||
|
* parser is linked; the lint driver calls back into
|
||||||
|
* pcbToolsLintBoard on the pcb side)
|
||||||
|
* kicad_tools --erc [--json] [--strict] <file.kicad_sch> [<out>]
|
||||||
|
* kicad_tools --netlist [--xml] <file.kicad_sch> [<out>]
|
||||||
|
* kicad_tools --bom <file.kicad_sch> [<out>]
|
||||||
|
* kicad_tools --plot [--pdf] <file.kicad_sch> [<out>]
|
||||||
|
*
|
||||||
|
* Each side brings up its own minimal PGM runtime on first use; dispatch is
|
||||||
|
* exclusive per process, so the two runtimes never coexist.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
int symConvertMain( int argc, char** argv );
|
||||||
|
int pcbConvertMain( int argc, char** argv );
|
||||||
|
|
||||||
|
|
||||||
|
int main( int argc, char** argv )
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
if( argc >= 2 && std::strcmp( argv[1], "--drc" ) == 0 )
|
||||||
|
rc = pcbConvertMain( argc, argv );
|
||||||
|
else
|
||||||
|
rc = symConvertMain( argc, argv );
|
||||||
|
|
||||||
|
// Skip the EXIT_RUNTIME static-dtor pass (same rationale as
|
||||||
|
// pcb_convert_main.cpp, which already does this on its --drc path): with
|
||||||
|
// the pcbnew kiface in the image, some dieted-out teardown is reachable
|
||||||
|
// through vtable slots from static dtors and traps ("table index is out
|
||||||
|
// of bounds") AFTER the subcommand finished, clobbering the exit code.
|
||||||
|
std::fflush( nullptr );
|
||||||
|
_exit( rc );
|
||||||
|
}
|
||||||
481
wasm/cli/pcb_convert_main.cpp
Normal file
481
wasm/cli/pcb_convert_main.cpp
Normal file
|
|
@ -0,0 +1,481 @@
|
||||||
|
/*
|
||||||
|
* pcb_convert — standalone KiCad board CLI, built as a WebAssembly module.
|
||||||
|
* The pcbnew-side sibling of sym_convert (pcbjam-mcp 0001 tier 3a).
|
||||||
|
*
|
||||||
|
* drc: pcb_convert --drc [--json] [--strict] <file.kicad_pcb> [<out>]
|
||||||
|
* Headless DRC. Replicates the scripting LoadBoard() path (the
|
||||||
|
* real one is Python-scripting code, stubbed to nullptr in WASM
|
||||||
|
* builds): PCB_IO_MGR::Load + DRC_ENGINE::InitEngine(.kicad_dru)
|
||||||
|
* + connectivity build, then runs DRC_ENGINE::RunTests the way
|
||||||
|
* kicad-cli's JobExportDrc does — no parity (needs the eeschema
|
||||||
|
* kiface over kiway), no zone refill (tools/ pruned; existing
|
||||||
|
* fills are checked as-is), no footprint-lib preload (the two
|
||||||
|
* library-parity tests are force-ignored).
|
||||||
|
* Writes a kicad-cli-compatible report (text, or JSON with
|
||||||
|
* --json) to <out> (default: <file>-drc.rpt/.json).
|
||||||
|
* Exit: 0 clean, 1 violations (--strict: also warnings +
|
||||||
|
* unconnected), 2 usage, 4 load/run failure.
|
||||||
|
*
|
||||||
|
* No GUI, no renderer, no embind bindings. Wired into pcbnew/CMakeLists.txt
|
||||||
|
* behind the KICAD_PCB_CONVERTER_WASM option (see
|
||||||
|
* scripts/kicad/build-pcb_convert.sh).
|
||||||
|
*
|
||||||
|
* GPL note: this is GPL KiCad code. The artifact is meant to be invoked as a
|
||||||
|
* separate process from closed code, never linked into it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
#include <wx/filename.h>
|
||||||
|
#include <wx/image.h>
|
||||||
|
#include <wx/init.h>
|
||||||
|
#include <wx/string.h>
|
||||||
|
|
||||||
|
#include <base_screen.h>
|
||||||
|
#include <board.h>
|
||||||
|
#include <board_design_settings.h>
|
||||||
|
#include <drc/drc_engine.h>
|
||||||
|
#include <drc/drc_item.h>
|
||||||
|
#include <drc/drc_report.h>
|
||||||
|
#include <ki_exception.h>
|
||||||
|
#include <layer_ids.h>
|
||||||
|
#include <lset.h>
|
||||||
|
#include <libraries/library_manager.h>
|
||||||
|
#include <pcb_io/pcb_io_mgr.h>
|
||||||
|
#include <pcb_marker.h>
|
||||||
|
#include <pgm_base.h>
|
||||||
|
#include <project.h>
|
||||||
|
#include <project/project_file.h>
|
||||||
|
#include <properties/property.h>
|
||||||
|
#include <properties/property_mgr.h>
|
||||||
|
#include <settings/settings_manager.h>
|
||||||
|
#include <widgets/report_severity.h>
|
||||||
|
|
||||||
|
// ── minimal KiCad runtime (mirror of sym_convert_main.cpp's LINT_PGM) ─────────
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
/** PCB_CONVERT_TRACE=1: stage prints for diagnosing hangs/traps in the field. */
|
||||||
|
void trace( const char* aMsg )
|
||||||
|
{
|
||||||
|
if( std::getenv( "PCB_CONVERT_TRACE" ) )
|
||||||
|
std::fprintf( stderr, "[trace] %s\n", aMsg );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DRC_PGM : public PGM_BASE
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void MacOpenFile( const wxString& ) override {}
|
||||||
|
|
||||||
|
void CreateSettingsManager()
|
||||||
|
{
|
||||||
|
m_settings_manager = std::make_unique<SETTINGS_MANAGER>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The full InitPgm() is deliberately not run (kiway/curl plumbing); but
|
||||||
|
// the DRC engine parallelizes through GetThreadPool(), which only exists
|
||||||
|
// after m_singleton.Init() — without it the pool call wanders on a
|
||||||
|
// null-object read (address 0 is readable linear memory under wasm).
|
||||||
|
void CreateSingleton()
|
||||||
|
{
|
||||||
|
m_singleton.Init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pgm().GetLibraryManager() returns *m_library_manager unchecked; an empty
|
||||||
|
// manager (no tables) is valid and keeps any stray lookup from wandering.
|
||||||
|
void CreateLibraryManager()
|
||||||
|
{
|
||||||
|
m_library_manager = std::make_unique<LIBRARY_MANAGER>();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
SETTINGS_MANAGER& kiRuntime()
|
||||||
|
{
|
||||||
|
static SETTINGS_MANAGER* s_manager = nullptr;
|
||||||
|
|
||||||
|
if( !s_manager )
|
||||||
|
{
|
||||||
|
// JSON settings need a writable config dir; keep it away from any real
|
||||||
|
// user config (0 = don't overwrite an explicit override).
|
||||||
|
setenv( "KICAD_CONFIG_HOME", "/tmp/pcb_convert-config", 0 );
|
||||||
|
|
||||||
|
// Pin the KiCad thread pool to one worker BEFORE the first
|
||||||
|
// ADVANCED_CFG::GetCfg() call latches the value: the link ships only
|
||||||
|
// -sPTHREAD_POOL_SIZE=2 preloaded pthread workers, and the default
|
||||||
|
// (0 = hardware_concurrency) would spawn a pool the node runtime can
|
||||||
|
// only grow after returning to the event loop — which a blocking CLI
|
||||||
|
// never does.
|
||||||
|
{
|
||||||
|
const char* configHome = std::getenv( "KICAD_CONFIG_HOME" );
|
||||||
|
wxFileName advCfg( wxString::FromUTF8( configHome ), wxS( "kicad_advanced" ) );
|
||||||
|
|
||||||
|
if( !advCfg.DirExists() )
|
||||||
|
advCfg.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
|
||||||
|
|
||||||
|
if( !advCfg.FileExists() )
|
||||||
|
{
|
||||||
|
if( FILE* f = std::fopen( advCfg.GetFullPath().ToUTF8(), "wb" ) )
|
||||||
|
{
|
||||||
|
std::fputs( "MaximumThreads=1\n", f );
|
||||||
|
std::fclose( f );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliberately leaked: ~PGM_BASE runs Destroy() (curl/sentry cleanup)
|
||||||
|
// from the EXIT_RUNTIME static-dtor pass.
|
||||||
|
trace( "kiRuntime: constructing DRC_PGM" );
|
||||||
|
DRC_PGM* pgm = new DRC_PGM();
|
||||||
|
SetPgm( pgm );
|
||||||
|
trace( "kiRuntime: constructing SETTINGS_MANAGER" );
|
||||||
|
pgm->CreateSettingsManager();
|
||||||
|
trace( "kiRuntime: singleton (thread pool)" );
|
||||||
|
pgm->CreateSingleton();
|
||||||
|
trace( "kiRuntime: library manager (empty)" );
|
||||||
|
pgm->CreateLibraryManager();
|
||||||
|
s_manager = &pgm->GetSettingsManager();
|
||||||
|
trace( "kiRuntime: ready" );
|
||||||
|
}
|
||||||
|
|
||||||
|
return *s_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── headless board load ───────────────────────────────────────────────────────
|
||||||
|
// Replica of the scripting LoadBoard() (pcbnew_scripting_helpers.cpp:153),
|
||||||
|
// which the WASM build stubs to nullptr (it lives behind KICAD_SCRIPTING and
|
||||||
|
// #includes Python.h). Prints diagnostics and returns null on failure.
|
||||||
|
|
||||||
|
BOARD* loadBoardHeadless( const char* aInPath )
|
||||||
|
{
|
||||||
|
wxFileName fn( wxString::FromUTF8( aInPath ) );
|
||||||
|
fn.MakeAbsolute();
|
||||||
|
const wxString absPath = fn.GetFullPath();
|
||||||
|
|
||||||
|
SETTINGS_MANAGER& manager = kiRuntime();
|
||||||
|
|
||||||
|
wxFileName pro( fn );
|
||||||
|
pro.SetExt( wxS( "kicad_pro" ) );
|
||||||
|
|
||||||
|
// A board can embed bitmap images in several formats.
|
||||||
|
wxInitAllImageHandlers();
|
||||||
|
|
||||||
|
// aSetActive=false: the set-active tail needs kiway plumbing that doesn't
|
||||||
|
// exist headless (mirrors sym_convert's lint loader).
|
||||||
|
trace( "loadBoardHeadless: LoadProject" );
|
||||||
|
manager.LoadProject( pro.FileExists() ? pro.GetFullPath() : wxString( wxEmptyString ), false );
|
||||||
|
PROJECT& project = manager.Prj();
|
||||||
|
|
||||||
|
BASE_SCREEN::m_DrawingSheetFileName = project.GetProjectFile().m_BoardDrawingSheetFile;
|
||||||
|
|
||||||
|
trace( "loadBoardHeadless: PCB_IO_MGR::Load" );
|
||||||
|
BOARD* brd = nullptr;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
brd = PCB_IO_MGR::Load( PCB_IO_MGR::KICAD_SEXP, absPath );
|
||||||
|
}
|
||||||
|
catch( PARSE_ERROR& pe )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "%s:%d:%d: error: %s\n", aInPath, pe.lineNumber, pe.byteIndex,
|
||||||
|
(const char*) pe.ParseProblem().ToUTF8() );
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
catch( const IO_ERROR& ioe )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "%s: error: %s\n", aInPath,
|
||||||
|
(const char*) ioe.Problem().ToUTF8() );
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !brd )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "%s: error: failed to load board\n", aInPath );
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom DRC rule conditions (A.Layer == 'F.Cu', …) resolve layer names
|
||||||
|
// through the property system's PCB_LAYER_ID enum map.
|
||||||
|
trace( "loadBoardHeadless: layer enum map" );
|
||||||
|
ENUM_MAP<PCB_LAYER_ID>& layerEnum = ENUM_MAP<PCB_LAYER_ID>::Instance();
|
||||||
|
|
||||||
|
layerEnum.Choices().Clear();
|
||||||
|
layerEnum.Undefined( UNDEFINED_LAYER );
|
||||||
|
|
||||||
|
for( PCB_LAYER_ID layer : LSET::AllLayersMask() )
|
||||||
|
{
|
||||||
|
layerEnum.Map( layer, LSET::Name( layer ) ); // canonical name
|
||||||
|
layerEnum.Map( layer, brd->GetLayerName( layer ) ); // user name
|
||||||
|
}
|
||||||
|
|
||||||
|
brd->SetProject( &project );
|
||||||
|
|
||||||
|
trace( "loadBoardHeadless: DRC_ENGINE InitEngine" );
|
||||||
|
BOARD_DESIGN_SETTINGS& bds = brd->GetDesignSettings();
|
||||||
|
bds.m_DRCEngine = std::make_shared<DRC_ENGINE>( brd, &bds );
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
wxFileName rules( pro );
|
||||||
|
rules.SetExt( wxS( "kicad_dru" ) );
|
||||||
|
bds.m_DRCEngine->InitEngine( rules );
|
||||||
|
}
|
||||||
|
catch( ... )
|
||||||
|
{
|
||||||
|
// Best efforts — implicit (board-settings) rules still apply.
|
||||||
|
std::fprintf( stderr, "%s: warning: custom DRC rules failed to load\n", aInPath );
|
||||||
|
}
|
||||||
|
|
||||||
|
for( PCB_MARKER* marker : brd->ResolveDRCExclusions( true ) )
|
||||||
|
brd->Add( marker );
|
||||||
|
|
||||||
|
trace( "loadBoardHeadless: BuildConnectivity" );
|
||||||
|
brd->BuildConnectivity();
|
||||||
|
brd->BuildListOfNets();
|
||||||
|
brd->SynchronizeNetsAndNetClasses( true );
|
||||||
|
|
||||||
|
// Component-class assignment rules from the project; without this,
|
||||||
|
// hasComponentClass() conditions in custom rules never match.
|
||||||
|
brd->SynchronizeComponentClasses( std::unordered_set<wxString>() );
|
||||||
|
|
||||||
|
brd->UpdateUserUnits( brd, nullptr );
|
||||||
|
|
||||||
|
return brd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── headless DRC ──────────────────────────────────────────────────────────────
|
||||||
|
// Mirrors PCBNEW_JOBS_HANDLER::JobExportDrc with the headless deltas: no
|
||||||
|
// parity (kiway/eeschema kiface), no zone refill (tool framework pruned), no
|
||||||
|
// footprint-lib preload (the two library tests are force-ignored), markers
|
||||||
|
// added straight to the board instead of through a BOARD_COMMIT.
|
||||||
|
|
||||||
|
int runDrc( const char* aInPath, bool aJson, bool aStrict, const char* aOutPath )
|
||||||
|
{
|
||||||
|
wxFileName fn( wxString::FromUTF8( aInPath ) );
|
||||||
|
fn.MakeAbsolute();
|
||||||
|
|
||||||
|
BOARD* brd = loadBoardHeadless( aInPath );
|
||||||
|
|
||||||
|
if( !brd )
|
||||||
|
return 4;
|
||||||
|
|
||||||
|
BOARD_DESIGN_SETTINGS& bds = brd->GetDesignSettings();
|
||||||
|
|
||||||
|
// Library-parity tests dereference Pgm().GetLibraryManager() adapters that
|
||||||
|
// have no tables headless; parity-with-schematic needs the eeschema kiface.
|
||||||
|
bds.m_DRCSeverities[ DRCE_LIB_FOOTPRINT_ISSUES ] = RPT_SEVERITY_IGNORE;
|
||||||
|
bds.m_DRCSeverities[ DRCE_LIB_FOOTPRINT_MISMATCH ] = RPT_SEVERITY_IGNORE;
|
||||||
|
|
||||||
|
std::shared_ptr<DRC_ENGINE> drcEngine = bds.m_DRCEngine;
|
||||||
|
|
||||||
|
drcEngine->SetViolationHandler(
|
||||||
|
[&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
|
||||||
|
const std::function<void( PCB_MARKER* )>& aPathGenerator )
|
||||||
|
{
|
||||||
|
PCB_MARKER* marker = new PCB_MARKER( aItem, aPos, aLayer );
|
||||||
|
aPathGenerator( marker );
|
||||||
|
brd->Add( marker );
|
||||||
|
} );
|
||||||
|
|
||||||
|
trace( "runDrc: RunTests" );
|
||||||
|
brd->RecordDRCExclusions();
|
||||||
|
brd->DeleteMARKERs( true, true );
|
||||||
|
drcEngine->RunTests( EDA_UNITS::MM, false /*aReportAllTrackErrors*/, false /*aTestFootprints*/ );
|
||||||
|
drcEngine->ClearViolationHandler();
|
||||||
|
|
||||||
|
// Update the exclusion status on any excluded markers that still exist.
|
||||||
|
brd->ResolveDRCExclusions( false );
|
||||||
|
|
||||||
|
auto markersProvider = std::make_shared<DRC_ITEMS_PROVIDER>(
|
||||||
|
brd, MARKER_BASE::MARKER_DRC, MARKER_BASE::MARKER_DRAWING_SHEET );
|
||||||
|
auto ratsnestProvider = std::make_shared<DRC_ITEMS_PROVIDER>( brd, MARKER_BASE::MARKER_RATSNEST );
|
||||||
|
auto fpWarningsProvider = std::make_shared<DRC_ITEMS_PROVIDER>( brd, MARKER_BASE::MARKER_PARITY );
|
||||||
|
|
||||||
|
markersProvider->SetSeverities( RPT_SEVERITY_ERROR | RPT_SEVERITY_WARNING );
|
||||||
|
ratsnestProvider->SetSeverities( RPT_SEVERITY_ERROR | RPT_SEVERITY_WARNING );
|
||||||
|
fpWarningsProvider->SetSeverities( RPT_SEVERITY_ERROR | RPT_SEVERITY_WARNING );
|
||||||
|
|
||||||
|
const int errors = markersProvider->GetCount( RPT_SEVERITY_ERROR );
|
||||||
|
const int warnings = markersProvider->GetCount( RPT_SEVERITY_WARNING );
|
||||||
|
const int unconnected = ratsnestProvider->GetCount();
|
||||||
|
|
||||||
|
wxString outPath;
|
||||||
|
|
||||||
|
if( aOutPath )
|
||||||
|
{
|
||||||
|
outPath = wxString::FromUTF8( aOutPath );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
wxFileName out( fn );
|
||||||
|
out.SetName( out.GetName() + wxS( "-drc" ) );
|
||||||
|
out.SetExt( aJson ? wxS( "json" ) : wxS( "rpt" ) );
|
||||||
|
outPath = out.GetFullPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
trace( "runDrc: writing report" );
|
||||||
|
DRC_REPORT reportWriter( brd, EDA_UNITS::MM, markersProvider, ratsnestProvider,
|
||||||
|
fpWarningsProvider );
|
||||||
|
|
||||||
|
const bool wrote = aJson ? reportWriter.WriteJsonReport( outPath )
|
||||||
|
: reportWriter.WriteTextReport( outPath );
|
||||||
|
|
||||||
|
if( !wrote )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "%s: error: unable to save DRC report to %s\n", aInPath,
|
||||||
|
(const char*) outPath.ToUTF8() );
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same convention as sym_convert --erc: errors fail; --strict also fails
|
||||||
|
// on warnings and unconnected items.
|
||||||
|
const bool failed = errors > 0 || ( aStrict && ( warnings > 0 || unconnected > 0 ) );
|
||||||
|
|
||||||
|
std::fprintf( stderr, "%s: %s (%d errors, %d warnings, %d unconnected) -> %s\n", aInPath,
|
||||||
|
failed ? "FAIL" : "OK", errors, warnings, unconnected,
|
||||||
|
(const char*) outPath.ToUTF8() );
|
||||||
|
|
||||||
|
return failed ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef KICAD_TOOLS_COMBINED
|
||||||
|
// Full-parse board lint for the merged image's --lint driver (which lives on
|
||||||
|
// the eeschema side, sym_convert_main.cpp): a bare PCB_IO_MGR::Load parse —
|
||||||
|
// no project, no DRC engine, no connectivity. Returns the footprint count, or
|
||||||
|
// -1 with aError filled ("path:line:col: error: ..." on parse errors).
|
||||||
|
int pcbToolsLintBoard( const char* aInPath, std::string& aError )
|
||||||
|
{
|
||||||
|
wxFileName fn( wxString::FromUTF8( aInPath ) );
|
||||||
|
fn.MakeAbsolute();
|
||||||
|
|
||||||
|
kiRuntime();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::unique_ptr<BOARD> brd( PCB_IO_MGR::Load( PCB_IO_MGR::KICAD_SEXP, fn.GetFullPath() ) );
|
||||||
|
|
||||||
|
if( !brd )
|
||||||
|
{
|
||||||
|
aError = std::string( aInPath ) + ": error: failed to load board";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) brd->Footprints().size();
|
||||||
|
}
|
||||||
|
catch( PARSE_ERROR& pe )
|
||||||
|
{
|
||||||
|
char buf[1024];
|
||||||
|
std::snprintf( buf, sizeof( buf ), "%s:%d:%d: error: %s", aInPath, pe.lineNumber,
|
||||||
|
pe.byteIndex, (const char*) pe.ParseProblem().ToUTF8() );
|
||||||
|
aError = buf;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
catch( const IO_ERROR& ioe )
|
||||||
|
{
|
||||||
|
aError = std::string( aInPath ) + ": error: " + std::string( ioe.Problem().ToUTF8() );
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
catch( const std::exception& e )
|
||||||
|
{
|
||||||
|
aError = std::string( aInPath ) + ": error: " + e.what();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // KICAD_TOOLS_COMBINED
|
||||||
|
|
||||||
|
|
||||||
|
// Under KICAD_TOOLS_COMBINED (the merged kicad_tools image) this TU compiles
|
||||||
|
// as a library: the entry point keeps its name and kicad_tools_main.cpp
|
||||||
|
// dispatches to it; the standalone pcb_convert build wraps it in main() below.
|
||||||
|
int pcbConvertMain( int argc, char** argv )
|
||||||
|
{
|
||||||
|
// Non-tty stderr is fully buffered under emscripten/musl; a CLI's stderr
|
||||||
|
// must be unbuffered — losing the error report is worse than the syscall
|
||||||
|
// cost.
|
||||||
|
setvbuf( stderr, nullptr, _IONBF, 0 );
|
||||||
|
|
||||||
|
wxInitializer initializer( argc, argv );
|
||||||
|
|
||||||
|
if( !initializer.IsOk() )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "pcb_convert: wxWidgets initialisation failed\n" );
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( argc >= 2 && std::strcmp( argv[1], "--drc" ) == 0 )
|
||||||
|
{
|
||||||
|
// The minimal headless runtime never registers app settings;
|
||||||
|
// GetAppSettings fails SOFT to defaults but wxFAIL_MSGs on every call.
|
||||||
|
// Report output must stay parseable — drop the asserts.
|
||||||
|
wxDisableAsserts();
|
||||||
|
|
||||||
|
bool json = false;
|
||||||
|
bool strict = false;
|
||||||
|
int arg = 2;
|
||||||
|
|
||||||
|
while( arg < argc && std::strncmp( argv[arg], "--", 2 ) == 0 )
|
||||||
|
{
|
||||||
|
if( std::strcmp( argv[arg], "--json" ) == 0 )
|
||||||
|
json = true;
|
||||||
|
else if( std::strcmp( argv[arg], "--strict" ) == 0 )
|
||||||
|
strict = true;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
|
||||||
|
arg++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( arg >= argc )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr,
|
||||||
|
"usage: pcb_convert --drc [--json] [--strict] <file.kicad_pcb> [<out>]\n" );
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* inPath = argv[arg++];
|
||||||
|
const char* outPath = arg < argc ? argv[arg] : nullptr;
|
||||||
|
|
||||||
|
int rc = 4;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
rc = runDrc( inPath, json, strict, outPath );
|
||||||
|
}
|
||||||
|
catch( const std::exception& e )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "%s: error: %s\n", inPath, e.what() );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the EXIT_RUNTIME static-dtor pass: some dieted-out editor
|
||||||
|
// teardown is still reachable through vtable slots from static dtors
|
||||||
|
// and traps ("table index is out of bounds") AFTER the report is
|
||||||
|
// written, clobbering the exit code. A CLI has nothing to tear down —
|
||||||
|
// flush and leave.
|
||||||
|
std::fflush( nullptr );
|
||||||
|
_exit( rc );
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fprintf( stderr, "usage: pcb_convert --drc [--json] [--strict] <file.kicad_pcb> [<out>]\n" );
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef KICAD_TOOLS_COMBINED
|
||||||
|
int main( int argc, char** argv )
|
||||||
|
{
|
||||||
|
return pcbConvertMain( argc, argv );
|
||||||
|
}
|
||||||
|
#endif
|
||||||
15
wasm/cli/pcb_convert_stubs.cpp
Normal file
15
wasm/cli/pcb_convert_stubs.cpp
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* pcb_convert link-order diet stubs (mirror of sym_convert_stubs.cpp).
|
||||||
|
*
|
||||||
|
* Listed before the kiface objects in the pcb_convert link so
|
||||||
|
* --allow-multiple-definition resolves duplicated definitions to these
|
||||||
|
* (first definition wins). Grows as the diet surfaces link/runtime needs;
|
||||||
|
* function references left dangling by the CMake prune are covered by
|
||||||
|
* -sERROR_ON_UNDEFINED_SYMBOLS=0 (they become aborting JS imports), so only
|
||||||
|
* data/typeinfo needs and behavior overrides belong here.
|
||||||
|
*
|
||||||
|
* Currently empty: the --drc path needs no overrides — board *save* and the
|
||||||
|
* plot/export surfaces are pruned outright, and Kiface() is deliberately left
|
||||||
|
* undefined so a stray call aborts loudly instead of wandering on a null
|
||||||
|
* settings object.
|
||||||
|
*/
|
||||||
|
|
@ -4,15 +4,46 @@
|
||||||
* Two modes, one binary (ysync 0009 §7 — the lint tier rides the dieted
|
* Two modes, one binary (ysync 0009 §7 — the lint tier rides the dieted
|
||||||
* converter instead of shipping a second wasm):
|
* converter instead of shipping a second wasm):
|
||||||
*
|
*
|
||||||
* convert: sym_convert <input.lib> <output.kicad_sym>
|
* convert: --convert-lib <input.lib> <output.kicad_sym>
|
||||||
* legacy (.lib) -> S-expression (.kicad_sym) symbol-library
|
* legacy (.lib) -> S-expression (.kicad_sym) symbol-library
|
||||||
* conversion via SCH_IO_MGR::ConvertLibrary. Unchanged behavior.
|
* conversion via SCH_IO_MGR::ConvertLibrary. Paths are absolutized
|
||||||
|
* here (the legacy plugin writes an empty lib on relative paths
|
||||||
|
* while exiting 0).
|
||||||
|
*
|
||||||
|
* erc: sym_convert --erc [--json] [--strict] <file.kicad_sch> [<out>]
|
||||||
|
* Headless ERC (pcbjam-mcp 0001 tier 3a). Loads the schematic with
|
||||||
|
* the EESCHEMA_HELPERS::LoadSchematic post-load tail — minus the
|
||||||
|
* SCH_COMMIT / TOOL_MANAGER / Kiface() cleanup chain this diet tree
|
||||||
|
* doesn't link — builds the connection graph, and runs ERC_TESTER
|
||||||
|
* the way kicad-cli's JobSchErc does (no edit frame, no cvpcb).
|
||||||
|
* Tests needing machinery absent headless are force-ignored:
|
||||||
|
* lib-symbol issues + footprint filters (null LIBRARY_MANAGER),
|
||||||
|
* footprint links (no cvpcb kiface), sim models (sim/ TUs pruned).
|
||||||
|
* Writes a kicad-cli-compatible report (text, or JSON with --json)
|
||||||
|
* to <out> (default: <file>-erc.rpt/.json next to the input).
|
||||||
|
* Exit: 0 clean, 1 ERC errors (--strict: also warnings), 2 usage,
|
||||||
|
* 4 load/run failure.
|
||||||
|
*
|
||||||
|
* netlist: sym_convert --netlist [--xml] <file.kicad_sch> [<out>]
|
||||||
|
* bom: sym_convert --bom <file.kicad_sch> [<out>]
|
||||||
|
* KiCad s-expr netlist (default), XML netlist (--xml), or XML BOM
|
||||||
|
* (GNL_OPT_BOM, kicad-cli's python-bom) — mirrors JobExportNetlist /
|
||||||
|
* JobExportPythonBom on the same headless loader as --erc. SPICE
|
||||||
|
* and the legacy vendor formats are not linked (sim/ pruned; the
|
||||||
|
* vendor emitters can be re-admitted in CMake on demand).
|
||||||
|
*
|
||||||
|
* plot: sym_convert --plot [--pdf] <file.kicad_sch> [<out>]
|
||||||
|
* SVG (one file per sheet, into <out> dir, default = input's dir)
|
||||||
|
* or a single multi-page PDF. SCH_PLOTTER + common plotter
|
||||||
|
* backends — no GAL/GL context; stroke-font text.
|
||||||
*
|
*
|
||||||
* lint: sym_convert --lint [--strict] <file> [<file>...]
|
* lint: sym_convert --lint [--strict] <file> [<file>...]
|
||||||
* "OK" = KiCad will load it (not necessarily load it UNCHANGED —
|
* "OK" = KiCad will load it (not necessarily load it UNCHANGED —
|
||||||
* KiCad normalizes while parsing). Per extension:
|
* KiCad normalizes while parsing). Per extension:
|
||||||
* .kicad_sch full parse (SCH_IO_KICAD_SEXPR)
|
* .kicad_sch full parse (SCH_IO_KICAD_SEXPR)
|
||||||
* .kicad_sym / .lib full library parse (EnumerateSymbolLib)
|
* .kicad_sym / .lib full library parse (EnumerateSymbolLib)
|
||||||
|
* .kicad_pcb full parse (PCB_IO_MGR — merged
|
||||||
|
* kicad_tools image only)
|
||||||
* other s-expr files structure-only (parens/strings/atoms)
|
* other s-expr files structure-only (parens/strings/atoms)
|
||||||
* Every s-expr input additionally gets the uuid lints: duplicate
|
* Every s-expr input additionally gets the uuid lints: duplicate
|
||||||
* (uuid) fields inside one node (KiCad keeps the last) and one
|
* (uuid) fields inside one node (KiCad keeps the last) and one
|
||||||
|
|
@ -41,6 +72,7 @@
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <wx/arrstr.h>
|
#include <wx/arrstr.h>
|
||||||
|
|
@ -50,6 +82,7 @@
|
||||||
|
|
||||||
#include <ki_exception.h>
|
#include <ki_exception.h>
|
||||||
#include <lib_symbol.h>
|
#include <lib_symbol.h>
|
||||||
|
#include <libraries/library_manager.h>
|
||||||
#include <pgm_base.h>
|
#include <pgm_base.h>
|
||||||
#include <project.h>
|
#include <project.h>
|
||||||
#include <settings/settings_manager.h>
|
#include <settings/settings_manager.h>
|
||||||
|
|
@ -59,8 +92,31 @@
|
||||||
#include <sch_io/sch_io_mgr.h>
|
#include <sch_io/sch_io_mgr.h>
|
||||||
#include <sch_screen.h>
|
#include <sch_screen.h>
|
||||||
#include <sch_sheet.h>
|
#include <sch_sheet.h>
|
||||||
|
#include <sch_sheet_path.h>
|
||||||
#include <schematic.h>
|
#include <schematic.h>
|
||||||
|
|
||||||
|
#ifdef KICAD_TOOLS_COMBINED
|
||||||
|
// Merged image only: full-parse board lint via the pcbnew side
|
||||||
|
// (pcb_convert_main.cpp) — the standalone eeschema tree has no pcbnew parser.
|
||||||
|
int pcbToolsLintBoard( const char* aInPath, std::string& aError );
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <connection_graph.h>
|
||||||
|
#include <drawing_sheet/ds_data_model.h>
|
||||||
|
#include <erc/erc.h>
|
||||||
|
#include <erc/erc_report.h>
|
||||||
|
#include <erc/erc_settings.h>
|
||||||
|
#include <filename_resolver.h>
|
||||||
|
#include <netlist_exporter_kicad.h>
|
||||||
|
#include <netlist_exporter_xml.h>
|
||||||
|
#include <reporter.h>
|
||||||
|
#include <sch_painter.h>
|
||||||
|
#include <sch_plotter.h>
|
||||||
|
#include <sch_reference_list.h>
|
||||||
|
#include <sch_rule_area.h>
|
||||||
|
#include <settings/color_settings.h>
|
||||||
|
#include <widgets/report_severity.h>
|
||||||
|
|
||||||
// ── minimal KiCad runtime for the schematic-load path ─────────────────────────
|
// ── minimal KiCad runtime for the schematic-load path ─────────────────────────
|
||||||
// LoadSchematicFile needs a SCHEMATIC with a PROJECT (settings manager), and
|
// LoadSchematicFile needs a SCHEMATIC with a PROJECT (settings manager), and
|
||||||
// ParseSchematic's tail calls Pgm().GetLanguageTag() (feeding the wasm
|
// ParseSchematic's tail calls Pgm().GetLanguageTag() (feeding the wasm
|
||||||
|
|
@ -88,6 +144,25 @@ public:
|
||||||
{
|
{
|
||||||
m_settings_manager = std::make_unique<SETTINGS_MANAGER>();
|
m_settings_manager = std::make_unique<SETTINGS_MANAGER>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The full InitPgm() is deliberately not run (kiway/curl plumbing); but
|
||||||
|
// CONNECTION_GRAPH::Recalculate's submit_loop divides by GetThreadPool()'s
|
||||||
|
// thread count, and the pool only exists after m_singleton.Init() —
|
||||||
|
// without it --erc wanders on a null-object read (address 0 is readable
|
||||||
|
// linear memory under wasm, so null derefs hang instead of trapping).
|
||||||
|
void CreateSingleton()
|
||||||
|
{
|
||||||
|
m_singleton.Init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same hazard class: netlist export's makeLibraries() (and the ERC lib
|
||||||
|
// checks) call Pgm().GetLibraryManager(), which returns *m_library_manager
|
||||||
|
// unchecked. An empty manager (no tables loaded) is valid — lookups just
|
||||||
|
// return nullopt and the netlist's <libraries/> section stays empty.
|
||||||
|
void CreateLibraryManager()
|
||||||
|
{
|
||||||
|
m_library_manager = std::make_unique<LIBRARY_MANAGER>();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -101,6 +176,29 @@ SETTINGS_MANAGER& kiRuntime()
|
||||||
// user config (0 = don't overwrite an explicit override).
|
// user config (0 = don't overwrite an explicit override).
|
||||||
setenv( "KICAD_CONFIG_HOME", "/tmp/sym_convert-config", 0 );
|
setenv( "KICAD_CONFIG_HOME", "/tmp/sym_convert-config", 0 );
|
||||||
|
|
||||||
|
// Pin the KiCad thread pool to one worker BEFORE the first
|
||||||
|
// ADVANCED_CFG::GetCfg() call latches the value: the link ships only
|
||||||
|
// -sPTHREAD_POOL_SIZE=2 preloaded pthread workers, and the default
|
||||||
|
// (0 = hardware_concurrency) would spawn a pool the node runtime can
|
||||||
|
// only grow after returning to the event loop — which a blocking CLI
|
||||||
|
// never does.
|
||||||
|
{
|
||||||
|
const char* configHome = std::getenv( "KICAD_CONFIG_HOME" );
|
||||||
|
wxFileName advCfg( wxString::FromUTF8( configHome ), wxS( "kicad_advanced" ) );
|
||||||
|
|
||||||
|
if( !advCfg.DirExists() )
|
||||||
|
advCfg.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
|
||||||
|
|
||||||
|
if( !advCfg.FileExists() )
|
||||||
|
{
|
||||||
|
if( FILE* f = std::fopen( advCfg.GetFullPath().ToUTF8(), "wb" ) )
|
||||||
|
{
|
||||||
|
std::fputs( "MaximumThreads=1\n", f );
|
||||||
|
std::fclose( f );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Deliberately leaked: ~PGM_BASE runs Destroy() (curl/sentry cleanup)
|
// Deliberately leaked: ~PGM_BASE runs Destroy() (curl/sentry cleanup)
|
||||||
// from the EXIT_RUNTIME static-dtor pass, which the diet stubs out.
|
// from the EXIT_RUNTIME static-dtor pass, which the diet stubs out.
|
||||||
trace( "kiRuntime: constructing LINT_PGM" );
|
trace( "kiRuntime: constructing LINT_PGM" );
|
||||||
|
|
@ -108,6 +206,10 @@ SETTINGS_MANAGER& kiRuntime()
|
||||||
SetPgm( pgm );
|
SetPgm( pgm );
|
||||||
trace( "kiRuntime: constructing SETTINGS_MANAGER" );
|
trace( "kiRuntime: constructing SETTINGS_MANAGER" );
|
||||||
pgm->CreateSettingsManager();
|
pgm->CreateSettingsManager();
|
||||||
|
trace( "kiRuntime: singleton (thread pool)" );
|
||||||
|
pgm->CreateSingleton();
|
||||||
|
trace( "kiRuntime: library manager (empty)" );
|
||||||
|
pgm->CreateLibraryManager();
|
||||||
s_manager = &pgm->GetSettingsManager();
|
s_manager = &pgm->GetSettingsManager();
|
||||||
trace( "kiRuntime: ready" );
|
trace( "kiRuntime: ready" );
|
||||||
}
|
}
|
||||||
|
|
@ -380,6 +482,7 @@ bool lintOneFile( const char* aPath, bool aStrict )
|
||||||
LINT_REPORT report;
|
LINT_REPORT report;
|
||||||
const char* tier = "structure only";
|
const char* tier = "structure only";
|
||||||
int symbolCount = -1;
|
int symbolCount = -1;
|
||||||
|
int footprintCount = -1;
|
||||||
|
|
||||||
// The structural walk + uuid lints run on every s-expr format; the legacy
|
// The structural walk + uuid lints run on every s-expr format; the legacy
|
||||||
// .lib format is not an s-expr, so it gets the full parse only.
|
// .lib format is not an s-expr, so it gets the full parse only.
|
||||||
|
|
@ -422,9 +525,20 @@ bool lintOneFile( const char* aPath, bool aStrict )
|
||||||
symbolCount = lintSymbolLib( absPath );
|
symbolCount = lintSymbolLib( absPath );
|
||||||
tier = "full parse";
|
tier = "full parse";
|
||||||
}
|
}
|
||||||
// Anything else (kicad_pcb, kicad_wks, kicad_pro …) stays
|
#ifdef KICAD_TOOLS_COMBINED
|
||||||
// structure-only: this binary links the eeschema parsers, not
|
else if( ext == wxS( "kicad_pcb" ) )
|
||||||
// pcbnew's. Native kicad-cli covers boards in container contexts.
|
{
|
||||||
|
std::string boardError;
|
||||||
|
footprintCount = pcbToolsLintBoard( aPath, boardError );
|
||||||
|
|
||||||
|
if( footprintCount < 0 )
|
||||||
|
report.errors.push_back( boardError );
|
||||||
|
else
|
||||||
|
tier = "full parse";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// Anything else (kicad_wks, kicad_pro …— and kicad_pcb outside
|
||||||
|
// the merged kicad_tools image) stays structure-only.
|
||||||
}
|
}
|
||||||
catch( PARSE_ERROR& pe ) // non-const: ParseProblem() is unqualified
|
catch( PARSE_ERROR& pe ) // non-const: ParseProblem() is unqualified
|
||||||
{
|
{
|
||||||
|
|
@ -456,16 +570,380 @@ bool lintOneFile( const char* aPath, bool aStrict )
|
||||||
std::fprintf( stderr, "%s: FAIL\n", aPath );
|
std::fprintf( stderr, "%s: FAIL\n", aPath );
|
||||||
else if( symbolCount >= 0 )
|
else if( symbolCount >= 0 )
|
||||||
std::fprintf( stderr, "%s: OK (%s, %d symbols)\n", aPath, tier, symbolCount );
|
std::fprintf( stderr, "%s: OK (%s, %d symbols)\n", aPath, tier, symbolCount );
|
||||||
|
else if( footprintCount >= 0 )
|
||||||
|
std::fprintf( stderr, "%s: OK (%s, %d footprints)\n", aPath, tier, footprintCount );
|
||||||
else
|
else
|
||||||
std::fprintf( stderr, "%s: OK (%s)\n", aPath, tier );
|
std::fprintf( stderr, "%s: OK (%s)\n", aPath, tier );
|
||||||
|
|
||||||
return !failed;
|
return !failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── shared headless schematic loader (pcbjam-mcp 0001 tier 3a) ───────────────
|
||||||
|
// Load + the EESCHEMA_HELPERS::LoadSchematic post-load tail, minus the
|
||||||
|
// SCHEMATIC::RecalculateConnections cleanup pass: that path constructs a
|
||||||
|
// SCH_COMMIT on a TOOL_MANAGER wired to Kiface().KifaceSettings(), none of
|
||||||
|
// which this diet tree links. Editor-saved files are already normalized; the
|
||||||
|
// connection graph is built directly, the way the cleanup pass's own tail
|
||||||
|
// does it. Prints diagnostics and returns null on failure.
|
||||||
|
|
||||||
|
std::unique_ptr<SCHEMATIC> loadSchematicHeadless( const char* aInPath )
|
||||||
|
{
|
||||||
|
wxFileName fn( wxString::FromUTF8( aInPath ) );
|
||||||
|
fn.MakeAbsolute();
|
||||||
|
const wxString absPath = fn.GetFullPath();
|
||||||
|
|
||||||
|
SETTINGS_MANAGER& manager = kiRuntime();
|
||||||
|
|
||||||
|
// ERC severities, exclusions, netclasses and text vars live in the sibling
|
||||||
|
// .kicad_pro — load it when present. aSetActive=false as in lintSchematicFile
|
||||||
|
// (the set-active tail needs the library manager / kiway plumbing).
|
||||||
|
wxFileName pro( fn );
|
||||||
|
pro.SetExt( wxS( "kicad_pro" ) );
|
||||||
|
trace( "loadSchematicHeadless: LoadProject" );
|
||||||
|
manager.LoadProject( pro.FileExists() ? pro.GetFullPath() : wxString( wxEmptyString ), false );
|
||||||
|
PROJECT& project = manager.Prj();
|
||||||
|
project.SetElem( PROJECT::ELEM::LEGACY_SYMBOL_LIBS, nullptr );
|
||||||
|
|
||||||
|
auto schematic = std::make_unique<SCHEMATIC>( &project );
|
||||||
|
schematic->Reset();
|
||||||
|
SCH_SHEET* defaultSheet = schematic->GetTopLevelSheet( 0 );
|
||||||
|
|
||||||
|
trace( "loadSchematicHeadless: LoadSchematicFile" );
|
||||||
|
IO_RELEASER<SCH_IO> pi( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
|
||||||
|
SCH_SHEET* root = nullptr;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
root = pi->LoadSchematicFile( absPath, schematic.get() );
|
||||||
|
}
|
||||||
|
catch( PARSE_ERROR& pe )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "%s:%d:%d: error: %s\n", aInPath, pe.lineNumber, pe.byteIndex,
|
||||||
|
(const char*) pe.ParseProblem().ToUTF8() );
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
catch( const IO_ERROR& ioe )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "%s: error: %s\n", aInPath,
|
||||||
|
(const char*) ioe.Problem().ToUTF8() );
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
schematic->AddTopLevelSheet( root ); // the SCHEMATIC dtor owns the hierarchy
|
||||||
|
schematic->RemoveTopLevelSheet( defaultSheet );
|
||||||
|
delete defaultSheet;
|
||||||
|
|
||||||
|
if( root->GetName().IsEmpty() )
|
||||||
|
root->SetName( wxS( "Root" ) );
|
||||||
|
|
||||||
|
trace( "loadSchematicHeadless: post-load fixups" );
|
||||||
|
SCH_SHEET_LIST sheetList = schematic->BuildSheetListSortedByPageNumbers();
|
||||||
|
SCH_SCREENS screens( schematic->Root() );
|
||||||
|
|
||||||
|
for( SCH_SCREEN* screen = screens.GetFirst(); screen; screen = screens.GetNext() )
|
||||||
|
screen->UpdateLocalLibSymbolLinks();
|
||||||
|
|
||||||
|
if( schematic->RootScreen()->GetFileFormatVersionAtLoad() < 20221002 )
|
||||||
|
sheetList.UpdateSymbolInstanceData( schematic->RootScreen()->GetSymbolInstances() );
|
||||||
|
|
||||||
|
sheetList.UpdateSheetInstanceData( schematic->RootScreen()->GetSheetInstances() );
|
||||||
|
|
||||||
|
if( schematic->RootScreen()->GetFileFormatVersionAtLoad() < 20230221 )
|
||||||
|
screens.FixLegacyPowerSymbolMismatches();
|
||||||
|
|
||||||
|
// MigrateSimModels() deliberately skipped: legacy sim-model migration lives
|
||||||
|
// in the pruned sim/ TUs (undefined here); ERCE_SIMULATION_MODEL is ignored
|
||||||
|
// in runErc for the same reason.
|
||||||
|
schematic->LoadVariants();
|
||||||
|
|
||||||
|
wxString projectName = project.GetProjectName();
|
||||||
|
|
||||||
|
if( projectName.IsEmpty() )
|
||||||
|
projectName = fn.GetName();
|
||||||
|
|
||||||
|
sheetList.CheckForMissingSymbolInstances( projectName );
|
||||||
|
screens.PruneOrphanedSymbolInstances( projectName, sheetList );
|
||||||
|
screens.PruneOrphanedSheetInstances( projectName, sheetList );
|
||||||
|
sheetList.AnnotatePowerSymbols();
|
||||||
|
|
||||||
|
schematic->ConnectionGraph()->Reset();
|
||||||
|
schematic->ResolveERCExclusionsPostUpdate();
|
||||||
|
schematic->SetSheetNumberAndCount();
|
||||||
|
schematic->RecomputeIntersheetRefs();
|
||||||
|
|
||||||
|
for( SCH_SHEET_PATH& sheet : sheetList )
|
||||||
|
{
|
||||||
|
sheet.UpdateAllScreenReferences();
|
||||||
|
sheet.LastScreen()->TestDanglingEnds( nullptr, nullptr );
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_set<SCH_SCREEN*> allScreens;
|
||||||
|
|
||||||
|
for( const SCH_SHEET_PATH& path : sheetList )
|
||||||
|
allScreens.insert( path.LastScreen() );
|
||||||
|
|
||||||
|
SCH_RULE_AREA::UpdateRuleAreasInScreens( allScreens, nullptr );
|
||||||
|
|
||||||
|
trace( "loadSchematicHeadless: ConnectionGraph Recalculate" );
|
||||||
|
schematic->ConnectionGraph()->Recalculate( sheetList, true );
|
||||||
|
|
||||||
|
return schematic;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Default output path: next to the input, optional suffix, new extension. */
|
||||||
|
wxString defaultOutPath( const wxFileName& aIn, const wxString& aSuffix, const wxString& aExt )
|
||||||
|
{
|
||||||
|
wxFileName out( aIn );
|
||||||
|
out.SetName( out.GetName() + aSuffix );
|
||||||
|
out.SetExt( aExt );
|
||||||
|
return out.GetFullPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── headless ERC ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
int runErc( const char* aInPath, bool aJson, bool aStrict, const char* aOutPath )
|
||||||
|
{
|
||||||
|
wxFileName fn( wxString::FromUTF8( aInPath ) );
|
||||||
|
fn.MakeAbsolute();
|
||||||
|
|
||||||
|
std::unique_ptr<SCHEMATIC> schematicHolder = loadSchematicHeadless( aInPath );
|
||||||
|
|
||||||
|
if( !schematicHolder )
|
||||||
|
return 4;
|
||||||
|
|
||||||
|
SCHEMATIC& schematic = *schematicHolder;
|
||||||
|
PROJECT& project = schematic.Project();
|
||||||
|
|
||||||
|
// Tests needing machinery this headless runtime doesn't have. Lib-symbol
|
||||||
|
// and footprint-filter tests dereference Pgm().GetLibraryManager() (never
|
||||||
|
// constructed on LINT_PGM); footprint links need the cvpcb kiface (RunTests
|
||||||
|
// also skips it on the null aCvPcb); sim models need the pruned sim/ TUs.
|
||||||
|
ERC_SETTINGS& ercSettings = schematic.ErcSettings();
|
||||||
|
ercSettings.SetSeverity( ERCE_LIB_SYMBOL_ISSUES, RPT_SEVERITY_IGNORE );
|
||||||
|
ercSettings.SetSeverity( ERCE_LIB_SYMBOL_MISMATCH, RPT_SEVERITY_IGNORE );
|
||||||
|
ercSettings.SetSeverity( ERCE_FOOTPRINT_FILTERS, RPT_SEVERITY_IGNORE );
|
||||||
|
ercSettings.SetSeverity( ERCE_FOOTPRINT_LINK_ISSUES, RPT_SEVERITY_IGNORE );
|
||||||
|
ercSettings.SetSeverity( ERCE_SIMULATION_MODEL, RPT_SEVERITY_IGNORE );
|
||||||
|
|
||||||
|
trace( "runErc: RunTests" );
|
||||||
|
ERC_TESTER tester( &schematic );
|
||||||
|
tester.RunTests( nullptr /*drawing sheet*/, nullptr /*edit frame*/, nullptr /*cvpcb*/,
|
||||||
|
&project, nullptr /*progress*/ );
|
||||||
|
|
||||||
|
auto provider = std::make_shared<SHEETLIST_ERC_ITEMS_PROVIDER>( &schematic );
|
||||||
|
provider->SetSeverities( RPT_SEVERITY_ERROR | RPT_SEVERITY_WARNING );
|
||||||
|
|
||||||
|
const int errors = provider->GetCount( RPT_SEVERITY_ERROR );
|
||||||
|
const int warnings = provider->GetCount( RPT_SEVERITY_WARNING );
|
||||||
|
|
||||||
|
wxString outPath;
|
||||||
|
|
||||||
|
if( aOutPath )
|
||||||
|
{
|
||||||
|
outPath = wxString::FromUTF8( aOutPath );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
wxFileName out( fn );
|
||||||
|
out.SetName( out.GetName() + wxS( "-erc" ) );
|
||||||
|
out.SetExt( aJson ? wxS( "json" ) : wxS( "rpt" ) );
|
||||||
|
outPath = out.GetFullPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
trace( "runErc: writing report" );
|
||||||
|
ERC_REPORT reportWriter( &schematic, EDA_UNITS::MM, provider );
|
||||||
|
const bool wrote = aJson ? reportWriter.WriteJsonReport( outPath )
|
||||||
|
: reportWriter.WriteTextReport( outPath );
|
||||||
|
|
||||||
|
if( !wrote )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "%s: error: unable to save ERC report to %s\n", aInPath,
|
||||||
|
(const char*) outPath.ToUTF8() );
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool failed = errors > 0 || ( aStrict && warnings > 0 );
|
||||||
|
|
||||||
|
std::fprintf( stderr, "%s: %s (%d errors, %d warnings) -> %s\n", aInPath,
|
||||||
|
failed ? "FAIL" : "OK", errors, warnings, (const char*) outPath.ToUTF8() );
|
||||||
|
|
||||||
|
return failed ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── headless netlist / BOM export ─────────────────────────────────────────────
|
||||||
|
// Mirrors EESCHEMA_JOBS_HANDLER::JobExportNetlist / JobExportPythonBom. Only
|
||||||
|
// the KiCad s-expr and XML emitters are linked (SPICE needs the pruned sim/
|
||||||
|
// TUs; the other legacy formats can be re-admitted on demand).
|
||||||
|
|
||||||
|
void warnAnnotationIssues( SCHEMATIC& aSchematic, const char* aInPath )
|
||||||
|
{
|
||||||
|
SCH_REFERENCE_LIST referenceList;
|
||||||
|
aSchematic.Hierarchy().GetSymbols( referenceList, SYMBOL_FILTER_ALL );
|
||||||
|
|
||||||
|
if( referenceList.GetCount() > 0 )
|
||||||
|
{
|
||||||
|
if( referenceList.CheckAnnotation(
|
||||||
|
[]( ERCE_T, const wxString&, SCH_REFERENCE*, SCH_REFERENCE* )
|
||||||
|
{
|
||||||
|
} ) > 0 )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "%s: warning: schematic has annotation errors\n", aInPath );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ERC_TESTER erc( &aSchematic );
|
||||||
|
|
||||||
|
if( erc.TestDuplicateSheetNames( false ) > 0 )
|
||||||
|
std::fprintf( stderr, "%s: warning: duplicate sheet names\n", aInPath );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int runNetlist( const char* aInPath, bool aXml, bool aBom, const char* aOutPath )
|
||||||
|
{
|
||||||
|
wxFileName fn( wxString::FromUTF8( aInPath ) );
|
||||||
|
fn.MakeAbsolute();
|
||||||
|
|
||||||
|
std::unique_ptr<SCHEMATIC> schematic = loadSchematicHeadless( aInPath );
|
||||||
|
|
||||||
|
if( !schematic )
|
||||||
|
return 4;
|
||||||
|
|
||||||
|
warnAnnotationIssues( *schematic, aInPath );
|
||||||
|
|
||||||
|
std::unique_ptr<NETLIST_EXPORTER_BASE> helper;
|
||||||
|
unsigned netlistOption = 0;
|
||||||
|
wxString outPath;
|
||||||
|
|
||||||
|
if( aBom )
|
||||||
|
{
|
||||||
|
helper = std::make_unique<NETLIST_EXPORTER_XML>( schematic.get() );
|
||||||
|
netlistOption = GNL_OPT_BOM;
|
||||||
|
outPath = aOutPath ? wxString::FromUTF8( aOutPath )
|
||||||
|
: defaultOutPath( fn, wxS( "-bom" ), wxS( "xml" ) );
|
||||||
|
}
|
||||||
|
else if( aXml )
|
||||||
|
{
|
||||||
|
helper = std::make_unique<NETLIST_EXPORTER_XML>( schematic.get() );
|
||||||
|
outPath = aOutPath ? wxString::FromUTF8( aOutPath )
|
||||||
|
: defaultOutPath( fn, wxEmptyString, wxS( "xml" ) );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
helper = std::make_unique<NETLIST_EXPORTER_KICAD>( schematic.get() );
|
||||||
|
outPath = aOutPath ? wxString::FromUTF8( aOutPath )
|
||||||
|
: defaultOutPath( fn, wxEmptyString, wxS( "net" ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
trace( "runNetlist: WriteNetlist" );
|
||||||
|
const bool ok = helper->WriteNetlist( outPath, netlistOption, CLI_REPORTER::GetInstance() );
|
||||||
|
|
||||||
|
std::fprintf( stderr, "%s: %s -> %s\n", aInPath, ok ? "OK" : "FAIL",
|
||||||
|
(const char*) outPath.ToUTF8() );
|
||||||
|
|
||||||
|
return ok ? 0 : 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── headless schematic plot (SVG / PDF) ───────────────────────────────────────
|
||||||
|
// Mirrors EESCHEMA_JOBS_HANDLER::JobExportPlot + its InitRenderSettings: the
|
||||||
|
// plot path is plotter-based (common/plotters), no GAL context; text renders
|
||||||
|
// through the already-linked stroke font engine.
|
||||||
|
|
||||||
|
int runPlot( const char* aInPath, bool aPdf, const char* aOutPath )
|
||||||
|
{
|
||||||
|
wxFileName fn( wxString::FromUTF8( aInPath ) );
|
||||||
|
fn.MakeAbsolute();
|
||||||
|
|
||||||
|
std::unique_ptr<SCHEMATIC> schematic = loadSchematicHeadless( aInPath );
|
||||||
|
|
||||||
|
if( !schematic )
|
||||||
|
return 4;
|
||||||
|
|
||||||
|
auto renderSettings = std::make_unique<SCH_RENDER_SETTINGS>();
|
||||||
|
|
||||||
|
// InitRenderSettings replica (default theme, no drawing-sheet override).
|
||||||
|
COLOR_SETTINGS* cs = ::GetColorSettings( wxEmptyString );
|
||||||
|
renderSettings->LoadColors( cs );
|
||||||
|
renderSettings->m_ShowHiddenPins = false;
|
||||||
|
renderSettings->m_ShowHiddenFields = false;
|
||||||
|
renderSettings->m_ShowPinAltIcons = false;
|
||||||
|
renderSettings->SetDefaultPenWidth( schematic->Settings().m_DefaultLineWidth );
|
||||||
|
renderSettings->m_LabelSizeRatio = schematic->Settings().m_LabelSizeRatio;
|
||||||
|
renderSettings->m_TextOffsetRatio = schematic->Settings().m_TextOffsetRatio;
|
||||||
|
renderSettings->m_PinSymbolSize = schematic->Settings().m_PinSymbolSize;
|
||||||
|
renderSettings->SetDashLengthRatio( schematic->Settings().m_DashedLineDashRatio );
|
||||||
|
renderSettings->SetGapLengthRatio( schematic->Settings().m_DashedLineGapRatio );
|
||||||
|
renderSettings->SetDefaultFont( wxEmptyString ); // stroke font (KiCad default)
|
||||||
|
renderSettings->SetMinPenWidth( 0 );
|
||||||
|
|
||||||
|
// Drawing sheet: project/schematic setting, else the built-in default.
|
||||||
|
{
|
||||||
|
wxString sheetPath = schematic->Settings().m_SchDrawingSheetFileName;
|
||||||
|
wxString msg;
|
||||||
|
FILENAME_RESOLVER resolve;
|
||||||
|
resolve.SetProject( &schematic->Project() );
|
||||||
|
resolve.SetProgramBase( &Pgm() );
|
||||||
|
|
||||||
|
wxString absolutePath =
|
||||||
|
resolve.ResolvePath( sheetPath, wxGetCwd(), { schematic->GetEmbeddedFiles() } );
|
||||||
|
|
||||||
|
if( !DS_DATA_MODEL::GetTheInstance().LoadDrawingSheet( absolutePath, &msg ) )
|
||||||
|
std::fprintf( stderr, "%s: warning: drawing sheet load: %s\n", aInPath,
|
||||||
|
(const char*) msg.ToUTF8() );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text bboxes may have been cached during load with no default font set.
|
||||||
|
SCH_SCREENS screens( schematic->Root() );
|
||||||
|
|
||||||
|
for( SCH_SCREEN* screen = screens.GetFirst(); screen; screen = screens.GetNext() )
|
||||||
|
{
|
||||||
|
for( SCH_ITEM* item : screen->Items() )
|
||||||
|
item->ClearCaches();
|
||||||
|
|
||||||
|
for( const auto& [libItemName, libSymbol] : screen->GetLibSymbols() )
|
||||||
|
libSymbol->ClearCaches();
|
||||||
|
}
|
||||||
|
|
||||||
|
SCH_PLOT_OPTS plotOpts;
|
||||||
|
plotOpts.m_plotAll = true;
|
||||||
|
plotOpts.m_plotDrawingSheet = true;
|
||||||
|
plotOpts.m_blackAndWhite = false;
|
||||||
|
|
||||||
|
if( aPdf )
|
||||||
|
{
|
||||||
|
// Single multi-page PDF.
|
||||||
|
plotOpts.m_outputFile = aOutPath ? wxString::FromUTF8( aOutPath )
|
||||||
|
: defaultOutPath( fn, wxEmptyString, wxS( "pdf" ) );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// One SVG per sheet into a directory (kicad-cli behavior).
|
||||||
|
plotOpts.m_outputDirectory =
|
||||||
|
aOutPath ? wxString::FromUTF8( aOutPath ) : fn.GetPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
trace( "runPlot: SCH_PLOTTER::Plot" );
|
||||||
|
REPORTER& reporter = CLI_REPORTER::GetInstance();
|
||||||
|
SCH_PLOTTER plotter( schematic.get() );
|
||||||
|
plotter.Plot( aPdf ? PLOT_FORMAT::PDF : PLOT_FORMAT::SVG, plotOpts, renderSettings.get(),
|
||||||
|
&reporter );
|
||||||
|
|
||||||
|
const bool failed = reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR );
|
||||||
|
|
||||||
|
std::fprintf( stderr, "%s: %s -> %s\n", aInPath, failed ? "FAIL" : "OK",
|
||||||
|
(const char*) ( aPdf ? plotOpts.m_outputFile : plotOpts.m_outputDirectory )
|
||||||
|
.ToUTF8() );
|
||||||
|
|
||||||
|
return failed ? 4 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
int main( int argc, char** argv )
|
// Under KICAD_TOOLS_COMBINED (the merged kicad_tools image) this TU compiles
|
||||||
|
// as a library: the entry point keeps its name and kicad_tools_main.cpp
|
||||||
|
// dispatches to it; the standalone sym_convert build wraps it in main() below.
|
||||||
|
int symConvertMain( int argc, char** argv )
|
||||||
{
|
{
|
||||||
// Non-tty stderr is fully buffered under emscripten/musl, so a trap or
|
// Non-tty stderr is fully buffered under emscripten/musl, so a trap or
|
||||||
// hang eats every diagnostic printed before it. A CLI's stderr must be
|
// hang eats every diagnostic printed before it. A CLI's stderr must be
|
||||||
|
|
@ -482,6 +960,98 @@ int main( int argc, char** argv )
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if( argc >= 2 && std::strcmp( argv[1], "--erc" ) == 0 )
|
||||||
|
{
|
||||||
|
// Same rationale as --lint: the minimal runtime never registers app
|
||||||
|
// settings, and report output must stay parseable.
|
||||||
|
wxDisableAsserts();
|
||||||
|
|
||||||
|
bool json = false;
|
||||||
|
bool strict = false;
|
||||||
|
int arg = 2;
|
||||||
|
|
||||||
|
while( arg < argc && std::strncmp( argv[arg], "--", 2 ) == 0 )
|
||||||
|
{
|
||||||
|
if( std::strcmp( argv[arg], "--json" ) == 0 )
|
||||||
|
json = true;
|
||||||
|
else if( std::strcmp( argv[arg], "--strict" ) == 0 )
|
||||||
|
strict = true;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
|
||||||
|
arg++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( arg >= argc )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr,
|
||||||
|
"usage: sym_convert --erc [--json] [--strict] <file.kicad_sch> [<out>]\n" );
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* inPath = argv[arg++];
|
||||||
|
const char* outPath = arg < argc ? argv[arg] : nullptr;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return runErc( inPath, json, strict, outPath );
|
||||||
|
}
|
||||||
|
catch( const std::exception& e )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "%s: error: %s\n", inPath, e.what() );
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( argc >= 2 && ( std::strcmp( argv[1], "--netlist" ) == 0
|
||||||
|
|| std::strcmp( argv[1], "--bom" ) == 0
|
||||||
|
|| std::strcmp( argv[1], "--plot" ) == 0 ) )
|
||||||
|
{
|
||||||
|
wxDisableAsserts();
|
||||||
|
|
||||||
|
const bool isBom = std::strcmp( argv[1], "--bom" ) == 0;
|
||||||
|
const bool isPlot = std::strcmp( argv[1], "--plot" ) == 0;
|
||||||
|
bool xml = false;
|
||||||
|
bool pdf = false;
|
||||||
|
int arg = 2;
|
||||||
|
|
||||||
|
while( arg < argc && std::strncmp( argv[arg], "--", 2 ) == 0 )
|
||||||
|
{
|
||||||
|
if( !isPlot && !isBom && std::strcmp( argv[arg], "--xml" ) == 0 )
|
||||||
|
xml = true;
|
||||||
|
else if( isPlot && std::strcmp( argv[arg], "--pdf" ) == 0 )
|
||||||
|
pdf = true;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
|
||||||
|
arg++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( arg >= argc )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "usage: sym_convert --netlist [--xml] <file.kicad_sch> [<out>]\n"
|
||||||
|
" sym_convert --bom <file.kicad_sch> [<out>]\n"
|
||||||
|
" sym_convert --plot [--pdf] <file.kicad_sch> [<out>]\n" );
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* inPath = argv[arg++];
|
||||||
|
const char* outPath = arg < argc ? argv[arg] : nullptr;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if( isPlot )
|
||||||
|
return runPlot( inPath, pdf, outPath );
|
||||||
|
|
||||||
|
return runNetlist( inPath, xml, isBom, outPath );
|
||||||
|
}
|
||||||
|
catch( const std::exception& e )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "%s: error: %s\n", inPath, e.what() );
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if( argc >= 2 && std::strcmp( argv[1], "--lint" ) == 0 )
|
if( argc >= 2 && std::strcmp( argv[1], "--lint" ) == 0 )
|
||||||
{
|
{
|
||||||
// The minimal headless runtime never registers app settings;
|
// The minimal headless runtime never registers app settings;
|
||||||
|
|
@ -512,26 +1082,45 @@ int main( int argc, char** argv )
|
||||||
return allOk ? 0 : 1;
|
return allOk ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( argc < 3 )
|
if( argc >= 2 && std::strcmp( argv[1], "--convert-lib" ) == 0 && argc >= 4 )
|
||||||
{
|
{
|
||||||
std::fprintf( stderr, "usage: sym_convert <input.lib> <output.kicad_sym>\n"
|
// The legacy plugin asserts on relative paths and (worse) writes an
|
||||||
" sym_convert --lint [--strict] <file> [<file>...]\n" );
|
// empty lib while still exiting 0 — absolutize here so callers can't
|
||||||
return 2;
|
// hit that class of bug.
|
||||||
|
wxFileName inFn( wxString::FromUTF8( argv[2] ) );
|
||||||
|
wxFileName outFn( wxString::FromUTF8( argv[3] ) );
|
||||||
|
inFn.MakeAbsolute();
|
||||||
|
outFn.MakeAbsolute();
|
||||||
|
|
||||||
|
// aOldFileProps = nullptr: no library-table properties; ConvertLibrary
|
||||||
|
// guesses the source format from the path and writes the SCH_KICAD format.
|
||||||
|
const bool ok = SCH_IO_MGR::ConvertLibrary( nullptr, inFn.GetFullPath(),
|
||||||
|
outFn.GetFullPath() );
|
||||||
|
|
||||||
|
if( ok )
|
||||||
|
{
|
||||||
|
std::fprintf( stderr, "convert-lib: OK %s -> %s\n", argv[2], argv[3] );
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fprintf( stderr, "convert-lib: FAILED to convert %s\n", argv[2] );
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wxString inPath = wxString::FromUTF8( argv[1] );
|
std::fprintf( stderr, "usage: kicad_tools --convert-lib <input.lib> <output.kicad_sym>\n"
|
||||||
const wxString outPath = wxString::FromUTF8( argv[2] );
|
" kicad_tools --lint [--strict] <file> [<file>...]\n"
|
||||||
|
" kicad_tools --erc [--json] [--strict] <file.kicad_sch> [<out>]\n"
|
||||||
// aOldFileProps = nullptr: no library-table properties; ConvertLibrary
|
" kicad_tools --netlist [--xml] <file.kicad_sch> [<out>]\n"
|
||||||
// guesses the source format from the path and writes the SCH_KICAD format.
|
" kicad_tools --bom <file.kicad_sch> [<out>]\n"
|
||||||
const bool ok = SCH_IO_MGR::ConvertLibrary( nullptr, inPath, outPath );
|
" kicad_tools --plot [--pdf] <file.kicad_sch> [<out>]\n"
|
||||||
|
" kicad_tools --drc [--json] [--strict] <file.kicad_pcb> [<out>]\n" );
|
||||||
if( ok )
|
return 2;
|
||||||
{
|
|
||||||
std::fprintf( stderr, "sym_convert: OK %s -> %s\n", argv[1], argv[2] );
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::fprintf( stderr, "sym_convert: FAILED to convert %s\n", argv[1] );
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef KICAD_TOOLS_COMBINED
|
||||||
|
int main( int argc, char** argv )
|
||||||
|
{
|
||||||
|
return symConvertMain( argc, argv );
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
|
||||||
73
wasm/tools/CMakeLists.txt
Normal file
73
wasm/tools/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# Merged headless KiCad CLI (kicad_tools) — pcbjam-mcp 0001 tier 3a.
|
||||||
|
#
|
||||||
|
# ONE node-WASM executable links BOTH dieted kifaces and carries every
|
||||||
|
# sym_convert + pcb_convert subcommand:
|
||||||
|
# kicad_tools <in.lib> <out.kicad_sym> (legacy convert)
|
||||||
|
# kicad_tools --lint | --erc | --netlist | --bom | --plot (eeschema side)
|
||||||
|
# kicad_tools --drc (pcbnew side)
|
||||||
|
#
|
||||||
|
# Added from the kicad fork's top-level CMakeLists.txt via
|
||||||
|
# add_subdirectory( ${KICAD_WASM_LAYER}/tools ) when EMSCRIPTEN AND
|
||||||
|
# KICAD_TOOLS_WASM — after eeschema/ and pcbnew/, so both kifaces' CACHE
|
||||||
|
# INTERNAL library lists exist here (same pattern as wasm/editor/). The tree
|
||||||
|
# must configure with BOTH diet options ON (KICAD_SYM_CONVERTER_WASM +
|
||||||
|
# KICAD_PCB_CONVERTER_WASM — build-kicad-target.sh passes all three flags).
|
||||||
|
#
|
||||||
|
# Collision note: the merged *editor* needs KICAD_WASM_PCB_SIDE_RENAMES
|
||||||
|
# (Kiface() + dialog/tool-layer classes, audited via llvm-nm intersection).
|
||||||
|
# Under the two CLI diets every TU in that audited set is pruned on at least
|
||||||
|
# one side — neither tree even defines Kiface() — so no renames here. Re-audit
|
||||||
|
# with scripts/kicad/audit-merged-symbols.sh on kicad version bumps.
|
||||||
|
#
|
||||||
|
# The standalone sym_convert / pcb_convert targets still exist in this
|
||||||
|
# (options-ON) tree but are built from their own per-app trees as before.
|
||||||
|
|
||||||
|
include_directories( BEFORE ${INC_BEFORE} )
|
||||||
|
include_directories(
|
||||||
|
${CMAKE_SOURCE_DIR}/common
|
||||||
|
${CMAKE_SOURCE_DIR}/pcbnew
|
||||||
|
${INC_AFTER}
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable( kicad_tools
|
||||||
|
# Link-order diet stubs: listed before the kiface objects so
|
||||||
|
# --allow-multiple-definition resolves duplicated definitions to these
|
||||||
|
# (first definition wins) — both sides' stub sets apply.
|
||||||
|
${KICAD_WASM_LAYER}/cli/sym_convert_stubs.cpp
|
||||||
|
${KICAD_WASM_LAYER}/cli/pcb_convert_stubs.cpp
|
||||||
|
${KICAD_WASM_LAYER}/cli/sym_convert_main.cpp
|
||||||
|
${KICAD_WASM_LAYER}/cli/pcb_convert_main.cpp
|
||||||
|
${KICAD_WASM_LAYER}/cli/kicad_tools_main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
# The per-app mains compile as libraries here: KICAD_TOOLS_COMBINED renames
|
||||||
|
# their entry points (symConvertMain / pcbConvertMain) and drops their own
|
||||||
|
# main(); kicad_tools_main.cpp dispatches between them.
|
||||||
|
target_compile_definitions( kicad_tools PRIVATE KICAD_TOOLS_COMBINED )
|
||||||
|
|
||||||
|
# Both kifaces' full library sets (exported CACHE INTERNAL by pcbnew/ and
|
||||||
|
# eeschema/). The overlap (common, kicommon, gal, ...) is deduped.
|
||||||
|
set( KICAD_TOOLS_LIBS ${PCBNEW_KIFACE_LIBRARIES} ${EESCHEMA_KIFACE_LIBRARIES} )
|
||||||
|
list( REMOVE_DUPLICATES KICAD_TOOLS_LIBS )
|
||||||
|
|
||||||
|
target_link_libraries( kicad_tools PRIVATE ${KICAD_TOOLS_LIBS} )
|
||||||
|
|
||||||
|
# Same link shape as pcb_convert: allow-multiple-definition for the wx/KiCad
|
||||||
|
# dupes and --whole-archive pcbcommon so RTTI-only-referenced vtables/typeinfo
|
||||||
|
# are pulled in.
|
||||||
|
target_link_options( kicad_tools PRIVATE
|
||||||
|
"LINKER:--allow-multiple-definition"
|
||||||
|
"LINKER:--whole-archive"
|
||||||
|
"$<TARGET_FILE:pcbcommon>"
|
||||||
|
"LINKER:--no-whole-archive"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Node CLI link shape — identical rationale to sym_convert (see
|
||||||
|
# eeschema/CMakeLists.txt): real FS, auto-run main(), synchronous, no embind,
|
||||||
|
# -g0 so wasm-emscripten-finalize doesn't OOM on DWARF, undefined symbols
|
||||||
|
# become aborting JS imports (both diets leave dangling refs from
|
||||||
|
# vtable-pinned-but-never-run editor code). The pre-js (shared with
|
||||||
|
# sym_convert) backs wxConfig with an in-memory store and copies process.env
|
||||||
|
# into the wasm ENV.
|
||||||
|
set_target_properties( kicad_tools PROPERTIES
|
||||||
|
LINK_FLAGS "-Oz -g0 --profiling-funcs -sSTACK_SIZE=2MB -sENVIRONMENT=node -sNODERAWFS=1 -sINVOKE_RUN=1 -sEXIT_RUNTIME=1 -sASYNCIFY=0 -sPTHREAD_POOL_SIZE=2 -sERROR_ON_UNDEFINED_SYMBOLS=0 --pre-js ${KICAD_WASM_LAYER}/cli/sym_convert_pre.js" )
|
||||||
Loading…
Reference in a new issue