Add TCL-embedded WASM build variant alongside the existing non-TCL build
Bump VERSION to 8.3.645.
magic.wasm can now be built as two variants packaged in the same npm
release: notcl/ (legacy, magic's own parser) and tcl/ (intubun/tcl 9.x
statically linked, commands evaluated by Tcl_EvalEx). The TCL fork is
pinned via npm/tcl.ref and cloned/built by magic itself — the tcl/
checkout is treated as read-only and built out-of-source into
magic/build-tcl-wasm/.
Configure layer:
- New usingTk variable decoupled from usingTcl in scripts/configure.in
+ scripts/configure, so --with-tcl --without-tk is finally a valid
combination. Native Linux Tcl+Tk builds keep their previous behaviour
(both flags default to enabled).
- When usingTk is empty, configure passes -DMAGIC_NO_TK so the small
number of remaining Tk callsites in tcltk/tclmagic.{h,c} compile out,
and TKCOMMON_SRCS / USE_TK_STUBS are omitted from the link.
WASM build orchestration:
- toolchains/emscripten/build-tcl-wasm.sh builds libtcl9.x.a + libtclstub.a
+ tclConfig.sh out-of-source from a pristine intubun/tcl checkout.
- npm/build.sh grew a --variant=<tcl|notcl|both> flag and writes its
outputs into npm/tcl/ and npm/notcl/. It also clones intubun/tcl with
autocrlf=false at the SHA pinned by npm/tcl.ref.
- magic/Makefile (WASM block only): magicWasm.o is now compiled with
DFLAGS_NOSTUB so Tcl_CreateInterp resolves to libtcl9.x directly
before tclStubsPtr is set. magic.js link pulls in LIB_SPECS_NOSTUB
and -ltclstub. After rules.mak include, magic: is a phony alias for
magic.js so the generic ${MODULE} recipe doesn't fight it.
- toolchains/emscripten/defs.mak: add -sUSE_ZLIB=1 (libtcl9 references
zlib), replace -sSTACK_SIZE=N with -Wl,-z,stack-size=N (emcc >=5
rejects the setting form).
- magic/magicWasm.c bootstraps the embedded interp under MAGIC_WRAPPER
(Tcl_CreateInterp -> Tcl_Init -> Tclmagic_Init) and routes
run_command through Tcl_EvalEx.
- magic/magicTop.c: gate MagicVersion/Revision/CompileTime on
!MAGIC_WRAPPER so they don't collide with the copies in
tcltk/tclmagic.c when both objects land in the same wasm binary.
npm package:
- Subpath exports: ".", "./tcl", "./notcl". Default import keeps the
pre-existing non-TCL behaviour for backward compatibility.
- examples/smoke-tcl.mjs exercises the TCL variant.
CI:
- main-wasm.yml clones intubun/tcl at the pinned ref, builds both
variants via npm/build.sh --variant=both, runs the existing notcl
test suite and the new TCL smoke test, and publishes only on a
v<x.y.z>... git tag. Tag name (minus the leading v) becomes the
npm version.
2026-05-17 21:41:03 +02:00
|
|
|
#!/usr/bin/env bash
|
2026-05-21 12:39:38 +02:00
|
|
|
# Build tcltk/tcl as a static WASM library for linking into magic.wasm.
|
Add TCL-embedded WASM build variant alongside the existing non-TCL build
Bump VERSION to 8.3.645.
magic.wasm can now be built as two variants packaged in the same npm
release: notcl/ (legacy, magic's own parser) and tcl/ (intubun/tcl 9.x
statically linked, commands evaluated by Tcl_EvalEx). The TCL fork is
pinned via npm/tcl.ref and cloned/built by magic itself — the tcl/
checkout is treated as read-only and built out-of-source into
magic/build-tcl-wasm/.
Configure layer:
- New usingTk variable decoupled from usingTcl in scripts/configure.in
+ scripts/configure, so --with-tcl --without-tk is finally a valid
combination. Native Linux Tcl+Tk builds keep their previous behaviour
(both flags default to enabled).
- When usingTk is empty, configure passes -DMAGIC_NO_TK so the small
number of remaining Tk callsites in tcltk/tclmagic.{h,c} compile out,
and TKCOMMON_SRCS / USE_TK_STUBS are omitted from the link.
WASM build orchestration:
- toolchains/emscripten/build-tcl-wasm.sh builds libtcl9.x.a + libtclstub.a
+ tclConfig.sh out-of-source from a pristine intubun/tcl checkout.
- npm/build.sh grew a --variant=<tcl|notcl|both> flag and writes its
outputs into npm/tcl/ and npm/notcl/. It also clones intubun/tcl with
autocrlf=false at the SHA pinned by npm/tcl.ref.
- magic/Makefile (WASM block only): magicWasm.o is now compiled with
DFLAGS_NOSTUB so Tcl_CreateInterp resolves to libtcl9.x directly
before tclStubsPtr is set. magic.js link pulls in LIB_SPECS_NOSTUB
and -ltclstub. After rules.mak include, magic: is a phony alias for
magic.js so the generic ${MODULE} recipe doesn't fight it.
- toolchains/emscripten/defs.mak: add -sUSE_ZLIB=1 (libtcl9 references
zlib), replace -sSTACK_SIZE=N with -Wl,-z,stack-size=N (emcc >=5
rejects the setting form).
- magic/magicWasm.c bootstraps the embedded interp under MAGIC_WRAPPER
(Tcl_CreateInterp -> Tcl_Init -> Tclmagic_Init) and routes
run_command through Tcl_EvalEx.
- magic/magicTop.c: gate MagicVersion/Revision/CompileTime on
!MAGIC_WRAPPER so they don't collide with the copies in
tcltk/tclmagic.c when both objects land in the same wasm binary.
npm package:
- Subpath exports: ".", "./tcl", "./notcl". Default import keeps the
pre-existing non-TCL behaviour for backward compatibility.
- examples/smoke-tcl.mjs exercises the TCL variant.
CI:
- main-wasm.yml clones intubun/tcl at the pinned ref, builds both
variants via npm/build.sh --variant=both, runs the existing notcl
test suite and the new TCL smoke test, and publishes only on a
v<x.y.z>... git tag. Tag name (minus the leading v) becomes the
npm version.
2026-05-17 21:41:03 +02:00
|
|
|
#
|
|
|
|
|
# This script does NOT modify the TCL source tree — the build is fully
|
|
|
|
|
# out-of-source. configure is invoked from the build directory inside magic,
|
|
|
|
|
# with the TCL source tree referenced through $0's path. All generated files
|
|
|
|
|
# (Makefile, objects, tclConfig.sh, libtcl9.x.a, ...) live under $OUT.
|
|
|
|
|
#
|
|
|
|
|
# Outputs (under $OUT):
|
|
|
|
|
# $OUT/Makefile, *.o, tclConfig.sh, libtcl9.x.a, libtclstub.a
|
|
|
|
|
# $OUT/install/include/{tcl.h, tclDecls.h, ...}
|
|
|
|
|
# $OUT/install/lib/{libtcl9.x.a, libtclstub.a, tclConfig.sh, tcl9.x/<scripts>}
|
|
|
|
|
#
|
|
|
|
|
# Usage:
|
|
|
|
|
# build-tcl-wasm.sh --src=<TCL source tree> [--out=<build dir>] [--clean]
|
|
|
|
|
#
|
|
|
|
|
# Requirements: an activated emsdk (emcc/emconfigure/emmake on PATH), a host
|
|
|
|
|
# gcc (used to build TCL's minizip helper, which runs natively), make, and a
|
2026-05-21 13:28:46 +02:00
|
|
|
# git checkout of tcltk/tcl pointed to by --src.
|
Add TCL-embedded WASM build variant alongside the existing non-TCL build
Bump VERSION to 8.3.645.
magic.wasm can now be built as two variants packaged in the same npm
release: notcl/ (legacy, magic's own parser) and tcl/ (intubun/tcl 9.x
statically linked, commands evaluated by Tcl_EvalEx). The TCL fork is
pinned via npm/tcl.ref and cloned/built by magic itself — the tcl/
checkout is treated as read-only and built out-of-source into
magic/build-tcl-wasm/.
Configure layer:
- New usingTk variable decoupled from usingTcl in scripts/configure.in
+ scripts/configure, so --with-tcl --without-tk is finally a valid
combination. Native Linux Tcl+Tk builds keep their previous behaviour
(both flags default to enabled).
- When usingTk is empty, configure passes -DMAGIC_NO_TK so the small
number of remaining Tk callsites in tcltk/tclmagic.{h,c} compile out,
and TKCOMMON_SRCS / USE_TK_STUBS are omitted from the link.
WASM build orchestration:
- toolchains/emscripten/build-tcl-wasm.sh builds libtcl9.x.a + libtclstub.a
+ tclConfig.sh out-of-source from a pristine intubun/tcl checkout.
- npm/build.sh grew a --variant=<tcl|notcl|both> flag and writes its
outputs into npm/tcl/ and npm/notcl/. It also clones intubun/tcl with
autocrlf=false at the SHA pinned by npm/tcl.ref.
- magic/Makefile (WASM block only): magicWasm.o is now compiled with
DFLAGS_NOSTUB so Tcl_CreateInterp resolves to libtcl9.x directly
before tclStubsPtr is set. magic.js link pulls in LIB_SPECS_NOSTUB
and -ltclstub. After rules.mak include, magic: is a phony alias for
magic.js so the generic ${MODULE} recipe doesn't fight it.
- toolchains/emscripten/defs.mak: add -sUSE_ZLIB=1 (libtcl9 references
zlib), replace -sSTACK_SIZE=N with -Wl,-z,stack-size=N (emcc >=5
rejects the setting form).
- magic/magicWasm.c bootstraps the embedded interp under MAGIC_WRAPPER
(Tcl_CreateInterp -> Tcl_Init -> Tclmagic_Init) and routes
run_command through Tcl_EvalEx.
- magic/magicTop.c: gate MagicVersion/Revision/CompileTime on
!MAGIC_WRAPPER so they don't collide with the copies in
tcltk/tclmagic.c when both objects land in the same wasm binary.
npm package:
- Subpath exports: ".", "./tcl", "./notcl". Default import keeps the
pre-existing non-TCL behaviour for backward compatibility.
- examples/smoke-tcl.mjs exercises the TCL variant.
CI:
- main-wasm.yml clones intubun/tcl at the pinned ref, builds both
variants via npm/build.sh --variant=both, runs the existing notcl
test suite and the new TCL smoke test, and publishes only on a
v<x.y.z>... git tag. Tag name (minus the leading v) becomes the
npm version.
2026-05-17 21:41:03 +02:00
|
|
|
#
|
|
|
|
|
# Note on line endings: if the TCL source tree was cloned on Windows with
|
|
|
|
|
# git's core.autocrlf=true, unix/configure may have CRLF line endings and
|
|
|
|
|
# bash will reject it. Clone with `git -c core.autocrlf=false clone ...` to
|
|
|
|
|
# avoid this; magic/npm/build.sh does that automatically.
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
# magic/ root is two levels up from toolchains/emscripten/.
|
|
|
|
|
MAGIC_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
|
|
|
|
|
|
|
TCL_SRC=""
|
|
|
|
|
OUT="$MAGIC_ROOT/build-tcl-wasm"
|
|
|
|
|
OPT_CLEAN=0
|
|
|
|
|
for arg in "$@"; do
|
|
|
|
|
case "$arg" in
|
|
|
|
|
--src=*) TCL_SRC=${arg#--src=} ;;
|
|
|
|
|
--out=*) OUT=${arg#--out=} ;;
|
|
|
|
|
--clean) OPT_CLEAN=1 ;;
|
|
|
|
|
*) echo "Unknown option: $arg" >&2; exit 1 ;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
if [ -z "$TCL_SRC" ]; then
|
|
|
|
|
echo "Error: --src=<tcl source tree> is required." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
if [ ! -f "$TCL_SRC/unix/configure" ]; then
|
|
|
|
|
echo "Error: $TCL_SRC/unix/configure not found — not a TCL source tree." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
# Detect CRLF in configure up-front so the failure mode is a useful error and
|
|
|
|
|
# not a cryptic `set: pipefail: invalid option name` from bash.
|
|
|
|
|
if head -1 "$TCL_SRC/unix/configure" | grep -q $'\r'; then
|
|
|
|
|
echo "Error: $TCL_SRC/unix/configure has CRLF line endings." >&2
|
2026-05-21 12:39:38 +02:00
|
|
|
echo " Reclone tcltk/tcl with: git -c core.autocrlf=false clone …" >&2
|
Add TCL-embedded WASM build variant alongside the existing non-TCL build
Bump VERSION to 8.3.645.
magic.wasm can now be built as two variants packaged in the same npm
release: notcl/ (legacy, magic's own parser) and tcl/ (intubun/tcl 9.x
statically linked, commands evaluated by Tcl_EvalEx). The TCL fork is
pinned via npm/tcl.ref and cloned/built by magic itself — the tcl/
checkout is treated as read-only and built out-of-source into
magic/build-tcl-wasm/.
Configure layer:
- New usingTk variable decoupled from usingTcl in scripts/configure.in
+ scripts/configure, so --with-tcl --without-tk is finally a valid
combination. Native Linux Tcl+Tk builds keep their previous behaviour
(both flags default to enabled).
- When usingTk is empty, configure passes -DMAGIC_NO_TK so the small
number of remaining Tk callsites in tcltk/tclmagic.{h,c} compile out,
and TKCOMMON_SRCS / USE_TK_STUBS are omitted from the link.
WASM build orchestration:
- toolchains/emscripten/build-tcl-wasm.sh builds libtcl9.x.a + libtclstub.a
+ tclConfig.sh out-of-source from a pristine intubun/tcl checkout.
- npm/build.sh grew a --variant=<tcl|notcl|both> flag and writes its
outputs into npm/tcl/ and npm/notcl/. It also clones intubun/tcl with
autocrlf=false at the SHA pinned by npm/tcl.ref.
- magic/Makefile (WASM block only): magicWasm.o is now compiled with
DFLAGS_NOSTUB so Tcl_CreateInterp resolves to libtcl9.x directly
before tclStubsPtr is set. magic.js link pulls in LIB_SPECS_NOSTUB
and -ltclstub. After rules.mak include, magic: is a phony alias for
magic.js so the generic ${MODULE} recipe doesn't fight it.
- toolchains/emscripten/defs.mak: add -sUSE_ZLIB=1 (libtcl9 references
zlib), replace -sSTACK_SIZE=N with -Wl,-z,stack-size=N (emcc >=5
rejects the setting form).
- magic/magicWasm.c bootstraps the embedded interp under MAGIC_WRAPPER
(Tcl_CreateInterp -> Tcl_Init -> Tclmagic_Init) and routes
run_command through Tcl_EvalEx.
- magic/magicTop.c: gate MagicVersion/Revision/CompileTime on
!MAGIC_WRAPPER so they don't collide with the copies in
tcltk/tclmagic.c when both objects land in the same wasm binary.
npm package:
- Subpath exports: ".", "./tcl", "./notcl". Default import keeps the
pre-existing non-TCL behaviour for backward compatibility.
- examples/smoke-tcl.mjs exercises the TCL variant.
CI:
- main-wasm.yml clones intubun/tcl at the pinned ref, builds both
variants via npm/build.sh --variant=both, runs the existing notcl
test suite and the new TCL smoke test, and publishes only on a
v<x.y.z>... git tag. Tag name (minus the leading v) becomes the
npm version.
2026-05-17 21:41:03 +02:00
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Normalise to absolute paths so the generated tclConfig.sh has stable paths.
|
|
|
|
|
TCL_SRC=$(cd "$TCL_SRC" && pwd)
|
|
|
|
|
mkdir -p "$OUT"
|
|
|
|
|
OUT=$(cd "$OUT" && pwd)
|
|
|
|
|
|
|
|
|
|
if [ $OPT_CLEAN -eq 1 ]; then
|
|
|
|
|
rm -rf "$OUT"
|
|
|
|
|
mkdir -p "$OUT"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
command -v emcc >/dev/null 2>&1 || {
|
|
|
|
|
echo "Error: emcc not on PATH. Activate emsdk first." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
echo "Using emcc: $(command -v emcc)"
|
|
|
|
|
emcc --version | head -1
|
|
|
|
|
|
|
|
|
|
ncpu() {
|
|
|
|
|
if command -v nproc >/dev/null 2>&1; then nproc
|
|
|
|
|
else echo 2
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cd "$OUT"
|
|
|
|
|
|
|
|
|
|
# --- configure --------------------------------------------------------------
|
|
|
|
|
# Standard autoconf out-of-source pattern: invoke configure from the build
|
|
|
|
|
# dir via its absolute path. configure uses dirname($0) as srcdir.
|
|
|
|
|
#
|
|
|
|
|
# -sUSE_ZLIB=1 makes emcc inject its zlib port so configure finds zlib.h /
|
|
|
|
|
# deflateSetHeader on the link line. Without it, TCL falls back to its
|
|
|
|
|
# compat/zlib copy whose include path isn't picked up by tclEvent.o.
|
|
|
|
|
#
|
|
|
|
|
# --disable-shared we statically link libtcl into magic.wasm.
|
|
|
|
|
# --disable-load no dynamic loading inside wasm.
|
|
|
|
|
# --enable-symbols=no release (-O2). Override CFLAGS to add -g for debug.
|
|
|
|
|
if [ ! -f Makefile ]; then
|
|
|
|
|
echo "=== emconfigure ==="
|
|
|
|
|
CFLAGS="-O2 -sUSE_ZLIB=1" \
|
|
|
|
|
emconfigure "$TCL_SRC/unix/configure" \
|
|
|
|
|
--disable-shared \
|
|
|
|
|
--disable-load \
|
|
|
|
|
--enable-symbols=no \
|
|
|
|
|
--host=wasm32-unknown-emscripten \
|
|
|
|
|
--prefix="$OUT/install"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- build ------------------------------------------------------------------
|
|
|
|
|
# HOST_CC/AR/RANLIB must be native — TCL's Makefile builds a `minizip` tool
|
|
|
|
|
# that runs on the host to produce the embedded zipfs resource. Without this
|
|
|
|
|
# override emcc would build minizip itself as a wasm module, which crashes on
|
|
|
|
|
# small stacks. HOST_OBJEXT is kept distinct from OBJEXT so host and target
|
|
|
|
|
# objects don't collide.
|
|
|
|
|
#
|
|
|
|
|
# Archive names come from tclConfig.sh so we don't bake in a fixed version.
|
|
|
|
|
TCL_LIB_FILE=$(. "$OUT/tclConfig.sh" && echo "$TCL_LIB_FILE")
|
|
|
|
|
TCL_STUB_LIB_FILE=$(. "$OUT/tclConfig.sh" && echo "$TCL_STUB_LIB_FILE")
|
|
|
|
|
HOST_OBJEXT=hostobj
|
|
|
|
|
|
|
|
|
|
echo "=== build ($TCL_LIB_FILE, $TCL_STUB_LIB_FILE) ==="
|
|
|
|
|
emmake make -j"$(ncpu)" \
|
|
|
|
|
HOST_CC=gcc HOST_AR=ar HOST_RANLIB=ranlib HOST_EXEEXT= HOST_OBJEXT="$HOST_OBJEXT" \
|
|
|
|
|
"$TCL_LIB_FILE" "$TCL_STUB_LIB_FILE"
|
|
|
|
|
|
|
|
|
|
# --- install ----------------------------------------------------------------
|
|
|
|
|
# install-headers populates $OUT/install/include with tcl.h + friends.
|
|
|
|
|
# install-libraries copies the Tcl script library (init.tcl, encodings, ...)
|
|
|
|
|
# under $OUT/install/lib/tcl9.x. We skip install-binaries because it would
|
|
|
|
|
# build a tclsh executable that we don't need; we cp the static archives
|
|
|
|
|
# manually so magic's configure (which scans <prefix>/lib for tclConfig.sh
|
|
|
|
|
# and a libtcl*.a) finds everything in one place.
|
|
|
|
|
emmake make \
|
|
|
|
|
HOST_CC=gcc HOST_AR=ar HOST_RANLIB=ranlib HOST_EXEEXT= HOST_OBJEXT="$HOST_OBJEXT" \
|
|
|
|
|
install-headers install-libraries
|
|
|
|
|
|
|
|
|
|
mkdir -p "$OUT/install/lib"
|
|
|
|
|
cp -f "$TCL_LIB_FILE" "$TCL_STUB_LIB_FILE" tclConfig.sh "$OUT/install/lib/"
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
echo "=== artifacts ==="
|
|
|
|
|
ls -la "$TCL_LIB_FILE" "$TCL_STUB_LIB_FILE" tclConfig.sh 2>&1 | sed 's/^/ /'
|
|
|
|
|
echo " install/lib:"
|
|
|
|
|
ls -la install/lib 2>&1 | sed 's/^/ /'
|