350 lines
14 KiB
Shell
Executable file
350 lines
14 KiB
Shell
Executable file
#!/bin/bash
|
|
w# Build ngspice for WebAssembly: sharedspice STATIC library + statically
|
|
# registered XSPICE code models. Consumed by the ngspice_service worker that
|
|
# backs KiCad eeschema's simulator.
|
|
#
|
|
# Deviations from a stock ngspice build, all load-bearing for wasm:
|
|
# - --with-ngshared + --disable-shared: libtool builds libngspice.a with the
|
|
# sharedspice API (ngSpice_Init & co.) compiled in and no CLI programs
|
|
# (bin_PROGRAMS is gated !SHARED_MODULE upstream, so no duplicate main()).
|
|
# - XSPICE code models (.cm) are dlopen'd at runtime natively, which a static
|
|
# wasm module cannot do. Under NGCM_STATIC the icm build archives each code
|
|
# model instead of linking a shared object (per-cm renamed table symbols via
|
|
# ngcm_dlmain_static.c), and a registry appended to dev.c resolves the seven
|
|
# bundled .cm basenames without dlopen. dlopen remains the fallback for
|
|
# unknown paths so user code models fail with ngspice's normal error text.
|
|
# Sources in scripts/deps/ngspice-wasm/; edits to the ngspice tree are
|
|
# idempotent (marker-guarded) since the tree is an extracted tarball.
|
|
# - /proc/meminfo header check is forced off: configure runs on the build
|
|
# host (Linux in docker), the browser runtime has no procfs, and ngspice's
|
|
# memory guard treats "0 bytes available" as out-of-memory.
|
|
# - -pthread everywhere: sharedspice's bg_run/bg_halt background thread is
|
|
# gated on HAVE_LIBPTHREAD; without it bg_run silently degrades to a
|
|
# synchronous blocking call.
|
|
# - Exception model must match the rest of the tree (DEPS_EH_FLAGS).
|
|
# - XSPICE + CIDER enabled: parity with native KiCad's bundled ngspice.
|
|
# - cmpp (XSPICE preprocessor) runs on the build host; ngspice's configure
|
|
# handles that itself when cross_compiling=yes (src/xspice/cmpp/build/).
|
|
|
|
set -e
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "${SCRIPT_DIR}/../common/env.sh"
|
|
source "${SCRIPT_DIR}/../common/versions.sh"
|
|
source "${SCRIPT_DIR}/../common/functions.sh"
|
|
|
|
NGSPICE_DIR="${DEPS_ROOT}/ngspice-${NGSPICE_VERSION}"
|
|
NGSPICE_BUILD="${BUILD_ROOT}/deps/ngspice"
|
|
NGSPICE_STAMP="${BUILD_ROOT}/stamps/ngspice.stamp"
|
|
NGCM_SRC_DIR="${SCRIPT_DIR}/ngspice-wasm"
|
|
|
|
# Parse arguments
|
|
CLEAN=0
|
|
for arg in "$@"; do
|
|
case $arg in
|
|
--clean)
|
|
CLEAN=1
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [ $CLEAN -eq 1 ]; then
|
|
log_info "Cleaning ngspice build..."
|
|
rm -rf "${NGSPICE_BUILD}" "${NGSPICE_STAMP}"
|
|
fi
|
|
|
|
# Check if already built
|
|
if check_stamp "${NGSPICE_STAMP}"; then
|
|
log_info "ngspice already built, skipping..."
|
|
exit 0
|
|
fi
|
|
|
|
# Download if needed
|
|
if [ ! -d "${NGSPICE_DIR}" ]; then
|
|
log_info "Downloading ngspice ${NGSPICE_VERSION}..."
|
|
mkdir -p "${DEPS_ROOT}"
|
|
cd "${DEPS_ROOT}"
|
|
|
|
download_file "${NGSPICE_URL}" "ngspice-${NGSPICE_VERSION}.tar.gz"
|
|
tar -xzf "ngspice-${NGSPICE_VERSION}.tar.gz"
|
|
rm "ngspice-${NGSPICE_VERSION}.tar.gz"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Static code-model support (NGCM_STATIC) - idempotent source edits
|
|
# ---------------------------------------------------------------------------
|
|
|
|
DEV_C="${NGSPICE_DIR}/src/spicelib/devices/dev.c"
|
|
ICM_MK="${NGSPICE_DIR}/src/xspice/icm/GNUmakefile.in"
|
|
|
|
# 1. Hook load_opus(): consult the registry before attempting dlopen.
|
|
# Must run BEFORE the registry append: the registry body contains the
|
|
# ngcm_static_load definition, so a plain grep for the name would mask the
|
|
# hook forever. The guard matches the call site only.
|
|
if ! grep -q "ngcm_static_load(name)" "${DEV_C}"; then
|
|
log_info "Inserting registry hook into load_opus()"
|
|
python3 - "${DEV_C}" <<'EOF'
|
|
import sys
|
|
|
|
path = sys.argv[1]
|
|
src = open(path).read()
|
|
anchor = " lib = dlopen(name, RTLD_NOW);"
|
|
hook = """#ifdef NGCM_STATIC
|
|
{
|
|
extern int ngcm_static_load(const char *path);
|
|
int ngcm_ret = ngcm_static_load(name);
|
|
if (ngcm_ret >= 0)
|
|
return ngcm_ret;
|
|
/* not a bundled code model: fall through to dlopen */
|
|
}
|
|
#endif
|
|
"""
|
|
assert src.count(anchor) == 1, "load_opus dlopen anchor not unique"
|
|
open(path, "w").write(src.replace(anchor, hook + anchor))
|
|
EOF
|
|
fi
|
|
|
|
# 2. Append the code-model registry to dev.c (marker-guarded).
|
|
if ! grep -q "NGCM_REGISTRY_MARKER" "${DEV_C}"; then
|
|
log_info "Appending static code-model registry to dev.c"
|
|
cat "${NGCM_SRC_DIR}/ngcm_registry.c" >> "${DEV_C}"
|
|
fi
|
|
|
|
# 3. Fix an upstream 32-bit union-punning bug in the CIDER card parser
|
|
# (found by the Gate-1 smoke, ngspice 46). The cleanup tests use
|
|
# `dataType & IF_REALVEC` (0x8004): every scalar IF_SET|IF_REAL parameter
|
|
# (0x2004) matches through the shared 0x4 bit, and the code then frees the
|
|
# vec-pointer union member overlaying the parsed scalar double. On 64-bit
|
|
# hosts that misread lands in zeroed padding after the double
|
|
# (free(NULL) no-op), which is why upstream never noticed; on wasm32 the
|
|
# pointer member overlays the HIGH HALF of the double and free() faults.
|
|
# TODO: upstream this to ngspice.
|
|
python3 - "${NGSPICE_DIR}/src/spicelib/parser/inpgmod.c" <<'EOF'
|
|
import sys
|
|
|
|
path = sys.argv[1]
|
|
src = open(path).read()
|
|
old = """ if (info->cardParms[idx].dataType & IF_STRING) {
|
|
FREE(value->sValue);
|
|
} else if (info->cardParms[idx].dataType & IF_REALVEC) {
|
|
FREE(value->v.vec.rVec);
|
|
} else if (info->cardParms[idx].dataType & IF_INTVEC) {
|
|
FREE(value->v.vec.iVec);
|
|
}"""
|
|
new = """ /* kicad-wasm: exact variant-type tests. The original
|
|
* `& IF_REALVEC` composite masks also match scalar
|
|
* IF_SET|IF_REAL parameters (shared 0x4 bit) and free the
|
|
* vec-pointer union member overlaying the scalar double -
|
|
* benign on 64-bit (lands in zero padding), heap fault on
|
|
* wasm32. */
|
|
int ngcm_vt = info->cardParms[idx].dataType & IF_VARTYPES;
|
|
if (ngcm_vt == IF_STRING) {
|
|
FREE(value->sValue);
|
|
} else if (ngcm_vt == IF_REALVEC) {
|
|
FREE(value->v.vec.rVec);
|
|
} else if (ngcm_vt == IF_INTVEC) {
|
|
FREE(value->v.vec.iVec);
|
|
}"""
|
|
if new in src:
|
|
pass # already applied
|
|
else:
|
|
assert src.count(old) == 1, "inpgmod.c cleanup-tests anchor not found"
|
|
open(path, "w").write(src.replace(old, new))
|
|
print("patched inpgmod.c IF_VARTYPES cleanup tests")
|
|
EOF
|
|
|
|
# 4. Redirect the icm build: archive code models instead of shared-linking,
|
|
# compile our renamed-tables TU instead of dlmain.c, and pass the cm name.
|
|
# All three edits are exact-string replacements, applied once.
|
|
python3 - "${ICM_MK}" <<'EOF'
|
|
import sys
|
|
|
|
path = sys.argv[1]
|
|
src = open(path).read()
|
|
|
|
edits = [
|
|
# .cm link recipe -> archive under NGCM_STATIC. The three common objects
|
|
# are excluded: dstring.o duplicates the core's, and the tline commons are
|
|
# shipped once via ngcm_common.a (they are prerequisites of every cm here
|
|
# but only the tlines models reference them).
|
|
("\t$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(VIS_CFLAGS) $(LDFLAGS) $^ $(LIBS) -o $@",
|
|
"\t$(if $(NGCM_STATIC),emar rcs $@ $(filter-out dstring.o msline_common.o tline_common.o,$^),$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(VIS_CFLAGS) $(LDFLAGS) $^ $(LIBS) -o $@)"),
|
|
# dlmain.o compiles our static-tables TU under NGCM_STATIC ($< follows the
|
|
# first prerequisite).
|
|
("$(cm)/dlmain.o : $(srcdir)/dlmain.c $(cm-descr)",
|
|
"$(cm)/dlmain.o : $(if $(NGCM_STATIC),$(NGCM_DLMAIN),$(srcdir)/dlmain.c) $(cm-descr)"),
|
|
# Per-cm symbol prefix for the tables TU (harmless for the other objects).
|
|
("COMPILE = $(CC) $(INCLUDES) -I$(cm) -I$(srcdir)/$(cm) $(CFLAGS) $(EXTRA_CFLAGS) $(VIS_CFLAGS)",
|
|
"COMPILE = $(CC) $(INCLUDES) -I$(cm) -I$(srcdir)/$(cm) $(CFLAGS) $(EXTRA_CFLAGS) $(VIS_CFLAGS) $(if $(NGCM_STATIC),-DNGCM_NAME=$(cm))"),
|
|
# The $(shell cmpp -p) model-list calls hardcode the in-tree cmpp, which
|
|
# is the CROSS-compiled (wasm) binary the build host cannot execute -
|
|
# the lists come back empty and the code models silently lose all their
|
|
# cfunc/ifspec objects. makedefs' CMPP is the host-built one under
|
|
# cross-compilation (configure.ac:1470-1475).
|
|
("""ifeq ($(OS),Windows_NT)
|
|
cmpp = ../cmpp/cmpp.exe
|
|
else
|
|
cmpp = ../cmpp/cmpp
|
|
endif""",
|
|
"cmpp = $(CMPP)"),
|
|
]
|
|
|
|
changed = False
|
|
for old, new in edits:
|
|
if new in src:
|
|
continue # already applied
|
|
assert old in src, f"icm GNUmakefile.in anchor not found: {old!r}"
|
|
src = src.replace(old, new)
|
|
changed = True
|
|
|
|
if changed:
|
|
open(path, "w").write(src)
|
|
print("patched icm GNUmakefile.in")
|
|
EOF
|
|
|
|
log_info "Building ngspice ${NGSPICE_VERSION} for WASM..."
|
|
|
|
mkdir -p "${NGSPICE_BUILD}"
|
|
cd "${NGSPICE_BUILD}"
|
|
|
|
# ngspice must run at real speed even in debug builds of the rest of the tree:
|
|
# the simulator is compute-bound and its own module is finalized separately.
|
|
if [ "${DEBUG_BUILD:-1}" = "1" ]; then
|
|
NGSPICE_OPT="-O2 -g"
|
|
else
|
|
NGSPICE_OPT="-O2"
|
|
fi
|
|
|
|
# -Wno-error guards: CIDER and parts of XSPICE are legacy C that modern clang
|
|
# (emcc >= llvm 16) rejects by default.
|
|
export CFLAGS="${NGSPICE_OPT} -pthread ${DEPS_EH_FLAGS} -DNGCM_STATIC \
|
|
-Wno-error=implicit-function-declaration -Wno-error=implicit-int"
|
|
export CXXFLAGS="${NGSPICE_OPT} -pthread ${DEPS_EH_FLAGS}"
|
|
export LDFLAGS="-pthread ${DEPS_EH_FLAGS}"
|
|
|
|
# Makefile-level knobs for the icm (code model) build, read from the
|
|
# environment by our GNUmakefile.in edits above.
|
|
export NGCM_STATIC=1
|
|
export NGCM_DLMAIN="${NGCM_SRC_DIR}/ngcm_dlmain_static.c"
|
|
|
|
emconfigure "${NGSPICE_DIR}/configure" \
|
|
--prefix="${SYSROOT}" \
|
|
--host=wasm32-unknown-emscripten \
|
|
--build="$("${NGSPICE_DIR}/config.guess")" \
|
|
--with-ngshared \
|
|
--disable-shared \
|
|
--enable-static \
|
|
--enable-xspice \
|
|
--enable-cider \
|
|
--disable-openmp \
|
|
--disable-debug \
|
|
--without-x \
|
|
--with-readline=no \
|
|
--without-editline \
|
|
ac_cv_header__proc_meminfo=no
|
|
|
|
# ngspice hardwires libtool's -shared mode for the ngshared build: configure
|
|
# sets STATIC=-shared (consumed as AM_CFLAGS by every convenience lib) and
|
|
# src/Makefile.am gives libngspice_la_{CFLAGS,LDFLAGS} a literal -shared.
|
|
# libtool refuses -shared outright on a target without shared-library support
|
|
# ("Fatal configuration error"), so force static mode: the make command line
|
|
# overrides $(STATIC) everywhere, and the two hardwired lines are rewritten in
|
|
# the generated Makefile (regenerated by configure on every build, so this
|
|
# stays idempotent).
|
|
sed -i.ngcm.bak \
|
|
-e 's/^\(libngspice_la_[A-Z]*FLAGS *=.*\)-shared/\1-static/' \
|
|
src/Makefile
|
|
|
|
# The XSPICE verilog/vhdl subdirs build VPI co-simulation shims (ivlng.la,
|
|
# ivlngvpi.la) that are inherently SHARED objects plugging into an external
|
|
# Icarus/GHDL process - impossible in wasm and unbuildable without shared-lib
|
|
# support. Skipping them matches native behaviour when no cosimulator is
|
|
# installed: the d_cosim code model fails at runtime with ngspice's normal
|
|
# error message.
|
|
sed -i.ngcm.bak \
|
|
-e 's/^\(SUBDIRS = mif cm enh evt idn cmpp icm\) verilog vhdl$/\1/' \
|
|
src/xspice/Makefile
|
|
|
|
emmake make -j${JOBS} STATIC=-static
|
|
emmake make install STATIC=-static
|
|
|
|
# dlmain.c's tail (fopen_with_path, cm_message_printf, cm_is_inertial) is
|
|
# utility code the cfunc objects call but that exists nowhere in the core -
|
|
# natively each .cm DLL carries its own copy. Extract it once from the
|
|
# pristine dlmain.c (BSD-3) so the seven archives stay collision-free; the
|
|
# coreitf wrapper section above the marker must NOT come along (it would
|
|
# shadow real core functions with calls through a never-initialized coreitf).
|
|
python3 - "${NGSPICE_DIR}/src/xspice/icm/dlmain.c" "${NGSPICE_BUILD}/ngcm_cmutil.c" <<'EOF'
|
|
import sys
|
|
|
|
src_path, out_path = sys.argv[1], sys.argv[2]
|
|
src = open(src_path).read()
|
|
marker = "#define DFLT_BUF_SIZE 256"
|
|
assert src.count(marker) == 1, "dlmain.c utility-tail marker not unique"
|
|
tail = src[src.index(marker):]
|
|
preamble = """/* Generated by build-ngspice.sh: utility tail of ngspice's
|
|
* src/xspice/icm/dlmain.c (BSD-3-Clause, Copyright 2000 The ngspice team),
|
|
* shared once across the statically linked code models. */
|
|
#include <stdarg.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "ngspice/config.h"
|
|
#include "ngspice/cpextern.h"
|
|
#include "ngspice/devdefs.h"
|
|
#include "ngspice/dstring.h"
|
|
#include "ngspice/dllitf.h"
|
|
#include "ngspice/evtudn.h"
|
|
#include "ngspice/inpdefs.h"
|
|
#include "ngspice/inertial.h"
|
|
#include "ngspice/cmproto.h"
|
|
|
|
/* In the DLL world cm_getvar is a dlmain.c wrapper that reaches the core's
|
|
* cp_getvar through coreitf (see cmexport.c binding dllitf_cm_getvar to
|
|
* cp_getvar). Statically there is no coreitf; bind it directly. */
|
|
bool cm_getvar(char *name, enum cp_types type, void *retval, size_t rsize)
|
|
{
|
|
return cp_getvar(name, type, retval, rsize);
|
|
}
|
|
|
|
"""
|
|
open(out_path, "w").write(preamble + tail)
|
|
EOF
|
|
|
|
emcc -c "${NGSPICE_BUILD}/ngcm_cmutil.c" -o "${NGSPICE_BUILD}/ngcm_cmutil.o" \
|
|
${CFLAGS} \
|
|
-I"${NGSPICE_DIR}/src/include" \
|
|
-I"${NGSPICE_BUILD}/src/include"
|
|
|
|
# The tline/msline common objects every cm build compiles but only the tlines
|
|
# models reference; shipped once so whole-archiving the .cm archives stays
|
|
# duplicate-free.
|
|
emar rcs "${SYSROOT}/lib/ngspice/ngcm_common.a" \
|
|
src/xspice/icm/msline_common.o \
|
|
src/xspice/icm/tline_common.o \
|
|
src/xspice/icm/dstring.o \
|
|
"${NGSPICE_BUILD}/ngcm_cmutil.o"
|
|
|
|
# Sanity: everything the ngspice_service link needs must exist.
|
|
for f in \
|
|
"${SYSROOT}/lib/libngspice.a" \
|
|
"${SYSROOT}/include/ngspice/sharedspice.h" \
|
|
"${SYSROOT}/lib/ngspice/analog.cm" \
|
|
"${SYSROOT}/lib/ngspice/digital.cm" \
|
|
"${SYSROOT}/lib/ngspice/spice2poly.cm" \
|
|
"${SYSROOT}/lib/ngspice/table.cm" \
|
|
"${SYSROOT}/lib/ngspice/tlines.cm" \
|
|
"${SYSROOT}/lib/ngspice/xtradev.cm" \
|
|
"${SYSROOT}/lib/ngspice/xtraevt.cm" \
|
|
"${SYSROOT}/share/ngspice/scripts/spinit"; do
|
|
if [ ! -f "$f" ]; then
|
|
log_error "ngspice install incomplete: missing $f"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
create_stamp "${NGSPICE_STAMP}"
|
|
log_info "ngspice build complete!"
|