diff --git a/.github/workflows/main-wasm.yml b/.github/workflows/main-wasm.yml index 8cf2ea15..ca4ae216 100644 --- a/.github/workflows/main-wasm.yml +++ b/.github/workflows/main-wasm.yml @@ -1,13 +1,16 @@ name: CI-wasm -# Builds the Magic WebAssembly target on every push and pull request. -# When the VERSION file changes on the default branch, the package is -# additionally published to GitHub Packages (npm.pkg.github.com) as -# @/magic-vlsi-wasm — no manual tag or token required. -# Tim Edwards updates VERSION to trigger a new release; the scope resolves -# automatically to the repo owner, so forks publish under their own namespace. +# Builds the Magic WebAssembly target (both the non-TCL and TCL variants) +# on every push and pull request as a CI check. **Publishing** only happens +# when a release tag of the form v... is pushed — that gate is the +# manual release trigger: # -# WASM is architecture-independent — built once on x86-64, usable everywhere. +# # bump magic/VERSION and/or npm/tcl.ref, commit, push to default branch +# git tag v8.3.638 +# git push origin v8.3.638 +# +# The tag name (minus the leading "v") becomes the published npm version. +# Forks publish under their own namespace via the @/ scope. on: push: @@ -70,32 +73,32 @@ jobs: echo "===== emcc -dM -E - ====="; echo | emcc -dM -E - | sort echo "===== em++ -dM -E - ====="; echo | em++ -dM -E - | sort - - name: Build WASM + # Clone intubun/tcl into a sibling directory at the pinned ref from + # npm/tcl.ref. npm/build.sh would do this on its own, but doing it as + # an explicit step makes the resolved SHA visible at the top of the + # job log and keeps the build step's output focused on the C build. + # The TCL source tree is treated as read-only — the actual WASM build + # runs inside magic (toolchains/emscripten/build-tcl-wasm.sh). + - name: Pin and clone intubun/tcl + run: | + . npm/tcl.ref + : "${TCL_REPO_URL:=https://github.com/intubun/tcl.git}" + : "${TCL_REF:=main}" + echo "Pinned TCL: $TCL_REF ($TCL_REPO_URL)" + # autocrlf=false: ubuntu-latest is already LF, but make it explicit. + git -c core.autocrlf=false clone "$TCL_REPO_URL" ../tcl + ( cd ../tcl && git checkout --detach "$TCL_REF" ) + + - name: Build WASM — both variants (tcl + notcl) run: | source ./emsdk/emsdk_env.sh - # --without/--disable flags: no WASM library available for these features - CFLAGS="--std=c17 -D_DEFAULT_SOURCE=1 -DEMSCRIPTEN=1" emconfigure ./configure \ - --without-cairo --without-opengl --without-x --without-tk --without-tcl \ - --disable-readline --disable-compression \ - --host=asmjs-unknown-emscripten \ - --target=asmjs-unknown-emscripten - # Append WASM linker flags and activate the WASM link target - cat toolchains/emscripten/defs.mak >> defs.mak - # Echo the merged defs.mak so CI logs show the exact build config - echo "===== defs.mak ====="; cat defs.mak; echo "===== defs.mak =====" - # Build in order: techs must exist before mains (--embed-file embeds them) - emmake make depend - emmake make -j$(nproc) modules libs - emmake make techs - emmake make mains + bash npm/build.sh --variant=both - - name: Copy WASM artifacts into npm/ - run: | - cp magic/magic.js npm/ - cp magic/magic.wasm npm/ + - name: Run example tests (non-TCL variant) + run: cd npm && npm test - - name: Run example tests - run: cd npm && npm run test + - name: Run smoke test (TCL variant) + run: cd npm && npm run test:tcl # Dump generated text outputs (.ext, .spice, .cif, …) into the CI log # so a regression in extraction / netlisting / cifoutput is visible @@ -112,21 +115,40 @@ jobs: esac done - - name: Set package version and scope + # The release gate. We publish a new npm version only when a tag of the + # shape v... is pushed. The tag name (minus the leading "v") is + # taken as the npm version verbatim — so `v8.3.638` → npm 8.3.638. + - name: Determine release version (tag-driven only) + id: release + run: | + if [ "${{ github.event_name }}" = "push" ] && \ + echo "${{ github.ref }}" | grep -Eq '^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+'; then + tag="${GITHUB_REF#refs/tags/}" + echo "publish=true" >> "$GITHUB_OUTPUT" + echo "version=${tag#v}" >> "$GITHUB_OUTPUT" + echo "Tag release: $tag → npm version ${tag#v}" + else + # For non-tag CI runs, use a dev-suffixed version so the packed + # tarball is still consumable for local inspection / artifact upload. + base=$(cat VERSION) + date=$(git show -s --format=%cs | tr -d '-') + hash=$(git show -s --format=%h) + echo "publish=false" >> "$GITHUB_OUTPUT" + echo "version=${base}-${date}.${hash}" >> "$GITHUB_OUTPUT" + echo "Non-tag build: will not publish." + fi + + - name: Set package version and scope + env: + VERSION: ${{ steps.release.outputs.version }} run: | - base=$(cat VERSION) # e.g. 8.3.637 - date=$(git show -s --format=%cs | tr -d '-') # e.g. 20260414 - hash=$(git show -s --format=%h) # e.g. d157eea - VERSION="${base}-${date}.${hash}" # Scope the package to the repo owner so it lands in the right # GitHub Packages namespace regardless of who hosts the repo. - # e.g. @rtimothyedwards/magic-vlsi-wasm on Tim's repo, - # @intubun/magic-vlsi-wasm on a fork. SCOPED_NAME="@${{ github.repository_owner }}/magic-vlsi-wasm" cd npm npm pkg set name="$SCOPED_NAME" npm pkg set publishConfig.registry="https://npm.pkg.github.com" - npm version "$VERSION" --no-git-tag-version + npm version "$VERSION" --no-git-tag-version --allow-same-version - name: Pack run: ./npm/pack.sh @@ -137,24 +159,8 @@ jobs: name: magic-vlsi-wasm-npm path: npm/*.tgz - - name: Check if VERSION changed - id: version_changed - if: github.event_name == 'push' - run: | - if echo "${{ github.ref }}" | grep -q '^refs/tags/'; then - echo "changed=true" >> $GITHUB_OUTPUT - elif [ "${{ github.ref }}" = "refs/heads/${{ github.event.repository.default_branch }}" ]; then - if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^VERSION$'; then - echo "changed=true" >> $GITHUB_OUTPUT - else - echo "changed=false" >> $GITHUB_OUTPUT - fi - else - echo "changed=false" >> $GITHUB_OUTPUT - fi - - name: Publish to GitHub Packages - if: steps.version_changed.outputs.changed == 'true' && github.event.inputs.dry_run != 'true' + if: steps.release.outputs.publish == 'true' && github.event.inputs.dry_run != 'true' run: cd npm && npm publish env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index c8beb02b..33650545 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ magic/magic.js magic/magic.js.symbols magic/magic.symbols magic/magic.wasm +build-tcl-wasm/ net2ir/net2ir net2ir/net2ir.js net2ir/net2ir.wasm diff --git a/magic/Makefile b/magic/Makefile index 87e23b1d..cefa8062 100644 --- a/magic/Makefile +++ b/magic/Makefile @@ -42,11 +42,25 @@ LIBS += ${GR_LIBS} ${READLINE_LIBS} -lm ${LD_EXTRA_LIBS} \ CLEANS += tclmagic${SHDLIB_EXT} libtclmagic${SHDLIB_EXT}.a proto.magicrc ifeq (${MAKE_WASM},1) -magic: magic.js +# magicWasm.c bootstraps the embedded Tcl interp by calling Tcl_CreateInterp / +# Tcl_Init before tclStubsPtr is initialised. With -DUSE_TCL_STUBS those calls +# expand to (*tclStubsPtr->...)() and dereference a NULL stubs pointer; so +# this one file must be compiled with DFLAGS_NOSTUB (= DFLAGS without +# -DUSE_TCL_STUBS). All other files keep using stubs. +magicWasm.o: magicWasm.c + @echo --- compiling magic/magicWasm.o '(no Tcl stubs)' + ${RM} magicWasm.o + ${CC} ${CFLAGS} ${CPPFLAGS} ${DFLAGS_NOSTUB} -c magicWasm.c + +# Pull in BOTH the main TCL archive (LIB_SPECS_NOSTUB → -ltcl9.x) and the +# stub bootstrap archive (-L${TCL_LIB_DIR} -ltclstub). Magic's source uses +# USE_TCL_STUBS macros, so tclStubsPtr (defined in libtclstub.a) and +# Tcl_InitStubs must be present in the same binary as the actual TCL +# implementation from libtcl9.x.a. magic.js: lib${MODULE}.o ${EXTRA_LIBS} @echo --- building main magic WASM ${RM} magic.js magic.wasm - ${CC} ${CFLAGS} ${CPPFLAGS} ${DFLAGS} lib${MODULE}.o ${EXTRA_LIBS} -o magic.js ${LIBS} + ${CC} ${CFLAGS} ${CPPFLAGS} ${DFLAGS} lib${MODULE}.o ${EXTRA_LIBS} -o magic.js ${LIBS} ${LIB_SPECS_NOSTUB} -L${TCL_LIB_DIR} -ltclstub endif main: magic proto.magicrc @@ -93,3 +107,14 @@ $(DESTDIR)${INSTALL_SYSDIR}/magicps.pro: magicps.pro ${CP} magicps.pro $(DESTDIR)${INSTALL_SYSDIR}/magicps.pro include ${MAGICDIR}/rules.mak + +ifeq (${MAKE_WASM},1) +# rules.mak defines `${MODULE}` (= `magic`) with a recipe that links a native +# executable without the TCL libraries. For the WASM build the real artifact +# is `magic.js` (+ `magic.wasm`), so override the target to be a phony alias +# that just rebuilds magic.js. Must come after the include so make uses this +# recipe instead of rules.mak's. +.PHONY: magic +magic: magic.js + @: +endif diff --git a/magic/magicTop.c b/magic/magicTop.c index 879b3eac..8573f98a 100644 --- a/magic/magicTop.c +++ b/magic/magicTop.c @@ -59,8 +59,14 @@ main(int argc, char *argv[]) * here, nor its format. It is updated by the Makefile in this directory. * * The version string originates at the top of scripts/config. + * + * Under MAGIC_WRAPPER (Tcl-embedded builds), tclmagic.c owns these globals; + * defining them here as well would produce duplicate-symbol errors when both + * objects end up in the same binary (as in the WASM build). */ +#ifndef MAGIC_WRAPPER char *MagicVersion = MAGIC_VERSION; char *MagicRevision = MAGIC_REVISION; char *MagicCompileTime = MAGIC_BUILDDATE; +#endif diff --git a/magic/magicWasm.c b/magic/magicWasm.c index e8a116a9..ebb8e5ef 100644 --- a/magic/magicWasm.c +++ b/magic/magicWasm.c @@ -8,6 +8,10 @@ #include #include +#ifdef MAGIC_WRAPPER +#include "tcltk/tclmagic.h" +#endif + #include "utils/main.h" #include "utils/magic.h" #include "utils/paths.h" @@ -38,6 +42,13 @@ magicWasmEnsureCadRoot(void) return 0; } +#ifdef MAGIC_WRAPPER +/* Forward decl — Tclmagic_Init installs all magic Tcl commands and calls + * Tcl_InitStubs(), which sets tclStubsPtr. Without this, any Tcl_X macro + * dereferences a NULL stubs pointer at runtime (crashes the wasm). */ +extern int Tclmagic_Init(Tcl_Interp *interp); +#endif + EMSCRIPTEN_KEEPALIVE int magic_wasm_init(void) { @@ -53,6 +64,40 @@ magic_wasm_init(void) if (magicWasmEnsureCadRoot() != 0) return -1; +#ifdef MAGIC_WRAPPER + /* In wrapper mode, magic's code (and our PaExpand path expansion) reaches + * for `magicinterp` to resolve $env vars via Tcl_GetVar. In the normal + * Linux flow Tclmagic_Init() is called by tclsh after dlopen(); here we + * embed the interp directly, so we have to bootstrap it before + * magicMainInit() runs anything that might touch Tcl. + * + * Note: we deliberately avoid TxError here — in MAGIC_WRAPPER mode + * TxError flushes via Tcl_EvalEx through tclStubsPtr, which only becomes + * non-NULL after Tclmagic_Init -> Tcl_InitStubs. So early errors go + * straight to stderr. */ + if (magicinterp == NULL) + { + Tcl_Interp *interp = Tcl_CreateInterp(); + if (interp == NULL) + { + fprintf(stderr, "magic_wasm_init: Tcl_CreateInterp returned NULL\n"); + return -1; + } + /* Tcl_Init loads /init.tcl from the Tcl library directory; in our + * embedded VFS that script isn't shipped, so failure here is expected + * and non-fatal — the interpreter itself is still usable for embedded + * evaluation, which is all we need. */ + (void)Tcl_Init(interp); + consoleinterp = interp; + if (Tclmagic_Init(interp) != TCL_OK) + { + fprintf(stderr, "magic_wasm_init: Tclmagic_Init failed: %s\n", + Tcl_GetStringResult(interp)); + return -1; + } + } +#endif + return magicMainInit(5, argv); } @@ -75,7 +120,15 @@ magic_wasm_run_command(const char *command) TxSetPoint(GrScreenRect.r_xtop / 2, GrScreenRect.r_ytop / 2, WIND_UNKNOWN_WINDOW); +#ifdef MAGIC_WRAPPER + /* In wrapper mode the command is Tcl. Evaluate it via the magic interp; + * the magic backend is reachable through ::magic:: ensemble commands. */ + if (magicinterp == NULL) + return -1; + return Tcl_EvalEx(magicinterp, command, -1, 0); +#else return TxDispatchString(command, FALSE); +#endif } EMSCRIPTEN_KEEPALIVE int diff --git a/npm/.gitignore b/npm/.gitignore index 228c6d22..2baee00c 100644 --- a/npm/.gitignore +++ b/npm/.gitignore @@ -1,5 +1,13 @@ +# Variant build outputs (regenerable via npm/build.sh). +tcl/magic.js +tcl/magic.wasm +notcl/magic.js +notcl/magic.wasm + +# Pre-restructure artifacts (just in case anyone still has them locally). magic.js magic.wasm + *.tgz node_modules/ package-lock.json diff --git a/npm/build.sh b/npm/build.sh index 4c4928c9..946a667e 100755 --- a/npm/build.sh +++ b/npm/build.sh @@ -2,9 +2,13 @@ # Build Magic WASM and copy artifacts into this npm/ directory. # # Usage: -# npm/build.sh [--release] [--test] [--pack] +# npm/build.sh [--variant=] [--release] [--test] [--pack] # -# --release Omit debug symbols (-g). +# --variant=tcl Build only the TCL-embedded variant → npm/tcl/ +# --variant=notcl Build only the plain (no Tcl/Tk) variant → npm/notcl/ +# --variant=both Build both (default) +# +# --release Omit debug symbols (-g) and build with -O2. # --test Run `npm run test` after copying artifacts. # --pack Run `npm pack` after copying artifacts (and tests, if given). # @@ -17,20 +21,33 @@ # EMSDK_DIR Path to an activated emsdk checkout. # If set, emsdk_env.sh is sourced from there. # If unset, emcc must already be on PATH (e.g. sourced externally). +# TCL_REPO Override the path to the intubun/tcl checkout (default: +# ../tcl relative to this magic checkout). Used by the TCL +# variant only. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(dirname "$SCRIPT_DIR")" +# The TCL variant builds against a sibling clone of intubun/tcl (pristine — +# magic never modifies it). The build itself happens inside magic under +# build-tcl-wasm/, so the TCL source tree stays clean. +TCL_REPO="${TCL_REPO:-$(dirname "$REPO_ROOT")/tcl}" +TCL_BUILD_DIR="${TCL_BUILD_DIR:-$REPO_ROOT/build-tcl-wasm}" +TCL_WASM_PREFIX="$TCL_BUILD_DIR/install" OPT_RELEASE=0 OPT_TEST=0 OPT_PACK=0 +OPT_VARIANT=both for arg in "$@"; do case "$arg" in - --release) OPT_RELEASE=1 ;; - --test) OPT_TEST=1 ;; - --pack) OPT_PACK=1 ;; + --release) OPT_RELEASE=1 ;; + --test) OPT_TEST=1 ;; + --pack) OPT_PACK=1 ;; + --variant=tcl) OPT_VARIANT=tcl ;; + --variant=notcl) OPT_VARIANT=notcl ;; + --variant=both) OPT_VARIANT=both ;; *) echo "Unknown option: $arg" >&2; exit 1 ;; esac done @@ -76,50 +93,120 @@ sed_strip_cr() { sed 's/\r//' "$file" > "$tmp" && cat "$tmp" > "$file" && rm "$tmp" } -# --- clean ------------------------------------------------------------------- -cd "$REPO_ROOT" - -# Only distclean if there's something to clean. A stale `|| true` here would -# hide real failures (e.g. broken toolchain) on a fresh checkout. -if [ -f defs.mak ]; then - emmake make distclean || true -fi -rm -f defs.mak database/database.h - -# --- configure --------------------------------------------------------------- - -# Strip Windows CRLF line endings (no-op on Linux-native files). -sed_strip_cr configure -find scripts/ -type f -print0 | while IFS= read -r -d '' f; do sed_strip_cr "$f"; done - if [ $OPT_RELEASE -eq 1 ]; then EXTRA_CFLAGS=" -O2" else EXTRA_CFLAGS=" -g" fi -CFLAGS="--std=c17 -D_DEFAULT_SOURCE=1 -DEMSCRIPTEN=1${EXTRA_CFLAGS}" \ - emconfigure ./configure \ - --without-cairo --without-opengl --without-x --without-tk --without-tcl \ - --disable-readline --disable-compression \ - --host=asmjs-unknown-emscripten \ - --target=asmjs-unknown-emscripten +# --- TCL fork: locate, pin, prebuild (TCL variant only) --------------------- +# Reads npm/tcl.ref to get the upstream URL + commit SHA. If the TCL source +# tree does not exist yet, clone it (with autocrlf=false to keep configure +# parseable on Windows hosts). If it does exist, just check out the pinned +# ref — no auto-fetch, so releases stay reproducible. +# +# The TCL source tree is treated as read-only. The actual WASM build runs in +# $TCL_BUILD_DIR (inside magic), driven by +# toolchains/emscripten/build-tcl-wasm.sh. +ensure_tcl_built() { + local TCL_REF_FILE="$SCRIPT_DIR/tcl.ref" + if [ -f "$TCL_REF_FILE" ]; then + # shellcheck source=/dev/null + . "$TCL_REF_FILE" + fi + : "${TCL_REPO_URL:=https://github.com/intubun/tcl.git}" + : "${TCL_REF:=main}" -cat toolchains/emscripten/defs.mak >> defs.mak + if [ ! -d "$TCL_REPO/.git" ]; then + echo "=== cloning $TCL_REPO_URL into $TCL_REPO ===" + git -c core.autocrlf=false clone "$TCL_REPO_URL" "$TCL_REPO" + fi -# --- build ------------------------------------------------------------------- -emmake make depend -emmake make -j"$(ncpu)" modules libs -emmake make techs -emmake make mains + ( cd "$TCL_REPO" + current_sha=$(git rev-parse HEAD 2>/dev/null || echo "") + if [ "$current_sha" != "$TCL_REF" ]; then + git fetch --quiet origin + git checkout --quiet --detach "$TCL_REF" + fi + echo "Using TCL at $(git rev-parse HEAD) ($TCL_REPO_URL)" + ) -# --- copy artifacts ---------------------------------------------------------- -cp magic/magic.js "$SCRIPT_DIR/" -cp magic/magic.wasm "$SCRIPT_DIR/" -echo "Copied magic.js and magic.wasm to npm/" + # Build TCL for WASM if it hasn't been built yet. The presence of + # tclConfig.sh in the install prefix is the canonical "TCL is built" marker. + if [ ! -f "$TCL_WASM_PREFIX/lib/tclConfig.sh" ]; then + echo "=== building TCL for WASM into $TCL_BUILD_DIR (one-time) ===" + bash "$REPO_ROOT/toolchains/emscripten/build-tcl-wasm.sh" \ + --src="$TCL_REPO" --out="$TCL_BUILD_DIR" + fi +} + +# --- build a single variant -------------------------------------------------- +# Each variant gets a fresh configure run because the two configurations +# select different code paths (MAGIC_WRAPPER on/off, MAGIC_NO_TK, link flags), +# so the object cache from one variant is not compatible with the other. +build_variant() { + local variant=$1 + local out_dir="$SCRIPT_DIR/$variant" + + echo + echo "===============================================================" + echo "=== building variant: $variant" + echo "===============================================================" + + cd "$REPO_ROOT" + + # Full clean — distclean removes the generated defs.mak and module objects. + if [ -f defs.mak ]; then + emmake make distclean || true + fi + rm -f defs.mak database/database.h + + # Strip Windows CRLF line endings (no-op on Linux-native files). + sed_strip_cr configure + find scripts/ -type f -print0 | while IFS= read -r -d '' f; do sed_strip_cr "$f"; done + + if [ "$variant" = "tcl" ]; then + ensure_tcl_built + CFLAGS="--std=c17 -D_DEFAULT_SOURCE=1 -DEMSCRIPTEN=1${EXTRA_CFLAGS}" \ + emconfigure ./configure \ + --without-cairo --without-opengl --without-x --without-tk \ + --with-tcl="$TCL_WASM_PREFIX/lib" \ + --with-tclincls="$TCL_WASM_PREFIX/include" \ + --with-tcllibs="$TCL_WASM_PREFIX/lib" \ + --disable-readline --disable-compression \ + --host=asmjs-unknown-emscripten \ + --target=asmjs-unknown-emscripten + else + CFLAGS="--std=c17 -D_DEFAULT_SOURCE=1 -DEMSCRIPTEN=1${EXTRA_CFLAGS}" \ + emconfigure ./configure \ + --without-cairo --without-opengl --without-x \ + --without-tk --without-tcl \ + --disable-readline --disable-compression \ + --host=asmjs-unknown-emscripten \ + --target=asmjs-unknown-emscripten + fi + + cat toolchains/emscripten/defs.mak >> defs.mak + + emmake make depend + emmake make -j"$(ncpu)" modules libs + emmake make techs + emmake make mains + + mkdir -p "$out_dir" + cp magic/magic.js "$out_dir/" + cp magic/magic.wasm "$out_dir/" + echo "Copied magic.js + magic.wasm into npm/$variant/" +} + +case "$OPT_VARIANT" in + tcl|notcl) build_variant "$OPT_VARIANT" ;; + both) build_variant notcl + build_variant tcl ;; +esac # --- optional test ----------------------------------------------------------- -# Runs the same smoke test that CI runs (see .github/workflows/main.yml). +# Runs the same smoke test that CI runs (see .github/workflows/main-wasm.yml). if [ $OPT_TEST -eq 1 ]; then cd "$SCRIPT_DIR" npm run test diff --git a/npm/examples/helpers.js b/npm/examples/helpers.js index 23772c0b..9092fb6c 100644 --- a/npm/examples/helpers.js +++ b/npm/examples/helpers.js @@ -1,11 +1,14 @@ // Shared utilities for Magic WASM examples. -import createMagicModule from '../magic.js'; +// +// These examples drive the non-TCL variant (legacy magic command parser). +// For Tcl-eval semantics, see examples/smoke-tcl.mjs. +import createMagicModule from '../notcl/magic.js'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve, basename } from 'node:path'; export const EXAMPLES_DIR = dirname(fileURLToPath(import.meta.url)); -export const wasmBinary = readFileSync(resolve(EXAMPLES_DIR, '../magic.wasm')); +export const wasmBinary = readFileSync(resolve(EXAMPLES_DIR, '../notcl/magic.wasm')); export const DEFAULT_TECH = 'scmos'; export const DEFAULT_MAG = resolve(EXAMPLES_DIR, 'min.mag'); export const DEFAULT_OUT = resolve(EXAMPLES_DIR, 'output'); diff --git a/npm/examples/smoke-tcl.mjs b/npm/examples/smoke-tcl.mjs new file mode 100644 index 00000000..6a007b74 --- /dev/null +++ b/npm/examples/smoke-tcl.mjs @@ -0,0 +1,39 @@ +// Smoke-test that confirms the TCL interpreter is live inside magic.wasm. +// +// In wrapper mode magic_wasm_run_command routes its argument to +// Tcl_EvalEx(magicinterp, ...). So: +// - pure Tcl (`set x 42; puts ...`) should work +// - magic commands are available as ::magic:: ensemble commands too +// +// Run: node npm/examples/smoke-tcl.mjs + +// Pull in the TCL-enabled variant explicitly via the /tcl subpath export. +import createMagic from '../tcl.js'; + +const m = await createMagic(); + +const status = m.init(); +if (status !== 0) { + console.error(`magic_wasm_init failed: ${status}`); + process.exit(1); +} +console.log('magic_wasm_init: OK'); + +function runTcl(label, command) { + const rc = m.runCommand(command); + console.log(`[rc=${rc}] ${label}: ${command}`); + return rc; +} + +// 1. Pure Tcl arithmetic — proves the TCL interp is parsing. +runTcl('tcl-set', 'set tcl_smoke_x 42'); +runTcl('tcl-expr', 'set tcl_smoke_y [expr {$tcl_smoke_x * 2}]'); + +// 2. Tcl introspection — magic should publish a Tclmagic package. +runTcl('tcl-info', 'puts "tcl_version=$tcl_version patchlevel=$tcl_patchLevel"'); +runTcl('tcl-pkgs', 'puts "packages=[package names]"'); + +// 3. A real magic command via the wrapper. +runTcl('magic-help', 'magic::help'); + +console.log('done'); diff --git a/npm/index.js b/npm/index.js index 9cb6e61b..517eba91 100644 --- a/npm/index.js +++ b/npm/index.js @@ -1,14 +1,24 @@ -import MagicModuleFactory from './magic.js'; +// Default entry point: the non-TCL build. +// +// This preserves the original API and behavior of magic-vlsi-wasm. Magic +// commands ("tech load sky130A", "load /work/inv", …) are dispatched through +// magic's legacy parser; no Tcl interpreter is involved. +// +// For the TCL-enabled build (commands are evaluated by Tcl_EvalEx, exposing +// the full Tcl 9 runtime alongside the ::magic:: command ensemble), import +// from "magic-vlsi-wasm/tcl" instead. + +import MagicModuleFactory from './notcl/magic.js'; async function createMagic(options = {}) { const module = await MagicModuleFactory(options); const init = module.cwrap('magic_wasm_init', 'number', []); - const runCommand = module.cwrap('magic_wasm_run_command', 'number', ['string']); - const sourceFile = module.cwrap('magic_wasm_source_file', 'number', ['string']); - const update = module.cwrap('magic_wasm_update', null, []); + const runCommand = module.cwrap('magic_wasm_run_command', 'number', ['string']); + const sourceFile = module.cwrap('magic_wasm_source_file', 'number', ['string']); + const update = module.cwrap('magic_wasm_update', null, []); - return { init, runCommand, sourceFile, update, FS: module.FS }; + return { init, runCommand, sourceFile, update, FS: module.FS, variant: 'notcl' }; } export { createMagic }; diff --git a/npm/notcl.js b/npm/notcl.js new file mode 100644 index 00000000..689cac3a --- /dev/null +++ b/npm/notcl.js @@ -0,0 +1,6 @@ +// Explicit non-TCL entry point: import from "magic-vlsi-wasm/notcl". +// Identical to the default `magic-vlsi-wasm` import — exists so callers can +// be explicit about which variant they want. + +export { createMagic } from './index.js'; +export { default } from './index.js'; diff --git a/npm/package.json b/npm/package.json index d4a82e17..e10338dc 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,34 +1,38 @@ { "name": "magic-vlsi-wasm", "version": "0.0.0-dev", - "description": "Magic VLSI Layout Tool — headless WebAssembly build", + "description": "Magic VLSI Layout Tool — headless WebAssembly build (TCL + non-TCL variants)", "type": "module", "main": "index.js", "types": "index.d.ts", "exports": { - ".": { - "import": "./index.js", - "types": "./index.d.ts" - } + ".": { "import": "./index.js", "types": "./index.d.ts" }, + "./tcl": { "import": "./tcl.js", "types": "./index.d.ts" }, + "./notcl": { "import": "./notcl.js", "types": "./index.d.ts" } }, "files": [ "index.js", "index.d.ts", - "magic.js", - "magic.wasm", + "tcl.js", + "notcl.js", + "tcl/magic.js", + "tcl/magic.wasm", + "notcl/magic.js", + "notcl/magic.wasm", "examples/", "LICENSE", "README.md" ], "scripts": { - "example": "node examples/extract.js", - "test": "node examples/all.js", - "test:gds": "node examples/gds.js", - "test:drc": "node examples/drc.js", - "test:cif": "node examples/cif.js" + "example": "node examples/extract.js", + "test": "node examples/all.js", + "test:tcl": "node examples/smoke-tcl.mjs", + "test:gds": "node examples/gds.js", + "test:drc": "node examples/drc.js", + "test:cif": "node examples/cif.js" }, - "keywords": ["magic", "vlsi", "eda", "wasm", "webassembly", "layout"], + "keywords": ["magic", "vlsi", "eda", "wasm", "webassembly", "layout", "tcl"], "license": "HPND", "engines": { "node": ">=18" diff --git a/npm/tcl.js b/npm/tcl.js new file mode 100644 index 00000000..f2b0cebb --- /dev/null +++ b/npm/tcl.js @@ -0,0 +1,33 @@ +// TCL-enabled entry point: import from "magic-vlsi-wasm/tcl". +// +// In this variant magic.wasm embeds a full Tcl 9 interpreter (from +// intubun/tcl, pinned via magic/npm/tcl.ref) and `runCommand(str)` calls +// Tcl_EvalEx(magicinterp, str, ...). Pure Tcl works: +// +// await magic.runCommand('set x 42'); +// await magic.runCommand('puts $tcl_version'); +// +// Magic commands are exposed as the ::magic:: ensemble: +// +// await magic.runCommand('magic::tech load sky130A'); +// await magic.runCommand('magic::load /work/inv'); +// +// (Bare command names like "tech load …" are not imported into the global +// namespace by this build — invoke them with the ::magic:: prefix, or set +// up `namespace import ::magic::*` yourself after init().) + +import MagicModuleFactory from './tcl/magic.js'; + +async function createMagic(options = {}) { + const module = await MagicModuleFactory(options); + + const init = module.cwrap('magic_wasm_init', 'number', []); + const runCommand = module.cwrap('magic_wasm_run_command', 'number', ['string']); + const sourceFile = module.cwrap('magic_wasm_source_file', 'number', ['string']); + const update = module.cwrap('magic_wasm_update', null, []); + + return { init, runCommand, sourceFile, update, FS: module.FS, variant: 'tcl' }; +} + +export { createMagic }; +export default createMagic; diff --git a/npm/tcl.ref b/npm/tcl.ref new file mode 100644 index 00000000..e14c074c --- /dev/null +++ b/npm/tcl.ref @@ -0,0 +1,16 @@ +# Pin for the TCL fork that the WASM build links against. +# +# Format: shell-style "VAR=VALUE" lines (no spaces around =). +# Lines starting with # or blank lines are ignored. +# +# To take a newer TCL release into magic-wasm: +# 1. Update TCL_REF below to the desired commit SHA (or tag/branch). +# 2. Bump magic/VERSION as usual. +# 3. Commit + push. CI rebuilds and republishes. +# +# Or use the `update-tcl` GitHub Actions workflow (workflow_dispatch only) to +# fetch a target ref from intubun/tcl and open a PR that just rewrites this +# file — you review and merge. + +TCL_REPO_URL=https://github.com/intubun/tcl.git +TCL_REF=84b23291b0dd811d642abef4ec7a55473c3eccb3 diff --git a/scripts/configure b/scripts/configure index fcb86a0e..1a24752f 100755 --- a/scripts/configure +++ b/scripts/configure @@ -6276,6 +6276,7 @@ magic_with_tk_libraries="" usingOGL=1 usingTcl=1 +usingTk=1 usingOA=0 usingCairo=1 usingPython3=1 @@ -7254,10 +7255,12 @@ if test "${with_tcl+set}" = set; then : magic_with_tcl=$withval if test "$withval" = "no" -o "$withval" = "NO"; then usingTcl= + usingTk= elif test $usingScheme ; then echo Attempt to enable both Tcl and Scheme interpreters. echo Disabling Tcl, and using Scheme instead. usingTcl= + usingTk= fi fi @@ -7268,6 +7271,9 @@ fi # Check whether --with-tk was given. if test "${with_tk+set}" = set; then : withval=$with_tk; magic_with_tk=$withval + if test "$withval" = "no" -o "$withval" = "NO"; then + usingTk= + fi fi @@ -7397,7 +7403,7 @@ fi # Find the Tk build configuration file "tkConfig.sh" # ----------------------------------------------------------------------- -if test $usingTcl ; then +if test $usingTk ; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking for tkConfig.sh" >&5 $as_echo_n "checking for tkConfig.sh... " >&6; } @@ -7477,8 +7483,8 @@ $as_echo "${tk_config_sh}" >&6; } if test "x$tk_config_sh" = "x" ; then echo "can't find Tk configuration script \"tkConfig.sh\"" - echo "Reverting to non-Tcl compilation" - usingTcl= + echo "Reverting to non-Tk compilation" + usingTk= fi fi @@ -7488,7 +7494,9 @@ fi if test $usingTcl ; then . $tcl_config_sh - . $tk_config_sh + if test $usingTk ; then + . $tk_config_sh + fi # Should probably trust the config file contents, but this configure # file checks the Tcl and Tk include and lib directories. Since @@ -7504,21 +7512,24 @@ if test $usingTcl ; then tmpstr=${TCL_LIB_SPEC#*-L} TCL_LIB_DIR=${tmpstr% -l*} - tmpstr=${TK_LIB_SPEC#*-L} - TK_LIB_DIR=${tmpstr% -l*} TCL_INC_DIR=${TCL_INCLUDE_SPEC#*-I} - TK_INC_DIR=${TK_INCLUDE_SPEC#*-I} + if test $usingTk ; then + tmpstr=${TK_LIB_SPEC#*-L} + TK_LIB_DIR=${tmpstr% -l*} + TK_INC_DIR=${TK_INCLUDE_SPEC#*-I} - if test "$TCL_VERSION" = "7.6" -a "$TK_VERSION" = "4.2" ; then - : - elif test "$TCL_VERSION" = "7.5" -a "$TK_VERSION" = "4.1" ; then - : - elif test "$TCL_VERSION" = "$TK_VERSION" ; then - : - else - echo "Mismatched Tcl/Tk versions ($TCL_VERSION != $TK_VERSION)" - echo "Reverting to non-Tcl compile" - usingTcl= + if test "$TCL_VERSION" = "7.6" -a "$TK_VERSION" = "4.2" ; then + : + elif test "$TCL_VERSION" = "7.5" -a "$TK_VERSION" = "4.1" ; then + : + elif test "$TCL_VERSION" = "$TK_VERSION" ; then + : + else + echo "Mismatched Tcl/Tk versions ($TCL_VERSION != $TK_VERSION)" + echo "Reverting to non-Tcl compile" + usingTcl= + usingTk= + fi fi fi @@ -7551,14 +7562,14 @@ if test $usingTcl ; then fi fi -if test $usingTcl ; then +if test $usingTk ; then if test "x${magic_with_tk_includes}" != "x" ; then if test -r "${magic_with_tk_includes}/tk.h" ; then TK_INC_DIR=${magic_with_tk_includes} else echo "Can't find tk.h in \"${magic_with_tk_includes}\"" - echo "Reverting to non-Tcl compile" - usingTcl= + echo "Reverting to non-Tk compile" + usingTk= fi else for dir in \ @@ -7575,8 +7586,8 @@ if test $usingTcl ; then done if test "x${TK_INC_DIR}" = "x" ; then echo "Can't find tk.h header file" - echo "Reverting to non-Tcl compile" - usingTcl= + echo "Reverting to non-Tk compile" + usingTk= fi fi fi @@ -7599,14 +7610,14 @@ if test $usingTcl ; then if test "x${TCL_LIB_SPEC}" = "x" ; then TCL_LIB_SPEC="-l${TCL_LIB_NAME}" fi - if test "x${TK_LIB_SPEC}" = "x" ; then + if test $usingTk -a "x${TK_LIB_SPEC}" = "x" ; then TK_LIB_SPEC="-l${TK_LIB_NAME}" fi # Find the version of "wish" that corresponds to TCL_EXEC_PREFIX # We really ought to run "ldd" to confirm that the linked libraries match. - if test "x${magic_with_wish_binary}" = "x" ; then + if test $usingTk -a "x${magic_with_wish_binary}" = "x" ; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking for wish executable" >&5 $as_echo_n "checking for wish executable... " >&6; } for dir in \ @@ -7638,7 +7649,7 @@ $as_echo "no" >&6; } { $as_echo "$as_me:${as_lineno-$LINENO}: result: ${WISH_EXE}" >&5 $as_echo "${WISH_EXE}" >&6; } fi - else + elif test $usingTk ; then WISH_EXE=${magic_with_wish_binary} fi @@ -7731,7 +7742,7 @@ $as_echo "${TCLSH_EXE}" >&6; } fi fi -if test $usingTcl ; then +if test $usingTk ; then if test "x${magic_with_tk_libraries}" != "x" ; then for libname in \ "${magic_with_tk_libraries}/${TCL_LIB_FILE}" \ @@ -7746,8 +7757,8 @@ if test $usingTcl ; then done if test "x${TK_LIB_DIR}" = "x" ; then echo "Can't find tk library in \"${magic_with_tk_libraries}\"" - echo "Reverting to non-Tcl compile" - usingTcl= + echo "Reverting to non-Tk compile" + usingTk= fi else for libname in \ @@ -7762,8 +7773,8 @@ if test $usingTcl ; then done if test "x${TK_LIB_DIR}" = "x" ; then echo "Can't find tk library" - echo "Reverting to non-Tcl compile" - usingTcl= + echo "Reverting to non-Tk compile" + usingTk= fi fi fi @@ -8638,7 +8649,12 @@ if test $usingTcl ; then extra_libs="$extra_libs \${MAGICDIR}/tcltk/libtcltk.o" extra_defs="$extra_defs -DTCL_DIR=\\\"\${TCLDIR}\\\"" - stub_defs="$stub_defs -DUSE_TCL_STUBS -DUSE_TK_STUBS" + if test $usingTk ; then + stub_defs="$stub_defs -DUSE_TCL_STUBS -DUSE_TK_STUBS" + else + stub_defs="$stub_defs -DUSE_TCL_STUBS -DMAGIC_NO_TK" + extra_defs="$extra_defs -DMAGIC_NO_TK" + fi elif test $usingScheme ; then modules="$modules lisp" unused="$unused tcltk" @@ -8681,7 +8697,9 @@ if test $usingTcl ; then gr_libs="$gr_libs -lX11" fi fi - gr_srcs="$gr_srcs \${TKCOMMON_SRCS}" + if test $usingTk ; then + gr_srcs="$gr_srcs \${TKCOMMON_SRCS}" + fi else if test $usingX11 ; then gr_dflags="$gr_dflags -DX11 -DXLIB" @@ -8791,23 +8809,25 @@ if test $usingTcl ; then # ----------------------------------------------------------------------- # -# Tk libraries and header files +# Tk libraries and header files (skipped under --without-tk) # # ----------------------------------------------------------------------- - if test "${TK_INC_DIR}" != "/usr/include" ; then - INC_SPECS="${INC_SPECS} -I${TK_INC_DIR}" - fi - if test "${TK_LIB_DIR}" = "/usr/lib" -o \ - "${TK_LIB_DIR}" = "/usr/lib64" ; then - LIB_SPECS_NOSTUB="${LIB_SPECS_NOSTUB} ${TK_LIB_SPEC}" - LIB_SPECS="${LIB_SPECS} ${TK_STUB_LIB_SPEC}" - else - LIB_SPECS_NOSTUB="${LIB_SPECS_NOSTUB} -L${TK_LIB_DIR} ${TK_LIB_SPEC}" - LIB_SPECS="${LIB_SPECS} -L${TK_LIB_DIR} ${TK_STUB_LIB_SPEC}" - if test "x${loader_run_path}" = "x" ; then - loader_run_path="${TK_LIB_DIR}" + if test $usingTk ; then + if test "${TK_INC_DIR}" != "/usr/include" ; then + INC_SPECS="${INC_SPECS} -I${TK_INC_DIR}" + fi + if test "${TK_LIB_DIR}" = "/usr/lib" -o \ + "${TK_LIB_DIR}" = "/usr/lib64" ; then + LIB_SPECS_NOSTUB="${LIB_SPECS_NOSTUB} ${TK_LIB_SPEC}" + LIB_SPECS="${LIB_SPECS} ${TK_STUB_LIB_SPEC}" else - loader_run_path="${TK_LIB_DIR}:${loader_run_path}" + LIB_SPECS_NOSTUB="${LIB_SPECS_NOSTUB} -L${TK_LIB_DIR} ${TK_LIB_SPEC}" + LIB_SPECS="${LIB_SPECS} -L${TK_LIB_DIR} ${TK_STUB_LIB_SPEC}" + if test "x${loader_run_path}" = "x" ; then + loader_run_path="${TK_LIB_DIR}" + else + loader_run_path="${TK_LIB_DIR}:${loader_run_path}" + fi fi fi diff --git a/scripts/configure.in b/scripts/configure.in index 98421feb..e2326fef 100644 --- a/scripts/configure.in +++ b/scripts/configure.in @@ -344,6 +344,7 @@ dnl disabled with --with-opengl=no usingOGL=1 usingTcl=1 +usingTk=1 usingOA=0 usingCairo=1 usingPython3=1 @@ -442,10 +443,12 @@ AC_ARG_WITH(tcl, magic_with_tcl=$withval if test "$withval" = "no" -o "$withval" = "NO"; then usingTcl= + usingTk= elif test $usingScheme ; then echo Attempt to enable both Tcl and Scheme interpreters. echo Disabling Tcl, and using Scheme instead. usingTcl= + usingTk= fi ], ) @@ -455,10 +458,17 @@ dnl and don't set the usingTcl variable. dnl dnl This has been broken up into a number of sections, each of which dnl depends independently on the setting of usingTcl. +dnl +dnl `usingTk` is independent of `usingTcl`: --without-tk enables +dnl Tcl-only embedding (used by the WASM build, which has no display +dnl server and cannot run Tk). dnl ---------------------------------------------------------------- AC_ARG_WITH(tk, [ --with-tk=DIR Find tkConfig.sh in DIR], - magic_with_tk=$withval) + magic_with_tk=$withval + if test "$withval" = "no" -o "$withval" = "NO"; then + usingTk= + fi) AC_ARG_WITH(tclincls, [ --with-tclincls=DIR Find tcl.h in DIR], magic_with_tcl_includes=$withval) AC_ARG_WITH(tkincls, [ --with-tkincls=DIR Find tk.h in DIR], @@ -564,7 +574,7 @@ fi # Find the Tk build configuration file "tkConfig.sh" # ----------------------------------------------------------------------- -if test $usingTcl ; then +if test $usingTk ; then AC_MSG_CHECKING([for tkConfig.sh]) tk_config_sh="" @@ -642,8 +652,8 @@ if test $usingTcl ; then if test "x$tk_config_sh" = "x" ; then echo "can't find Tk configuration script \"tkConfig.sh\"" - echo "Reverting to non-Tcl compilation" - usingTcl= + echo "Reverting to non-Tk compilation" + usingTk= fi fi @@ -653,7 +663,9 @@ fi if test $usingTcl ; then . $tcl_config_sh - . $tk_config_sh + if test $usingTk ; then + . $tk_config_sh + fi # Should probably trust the config file contents, but this configure # file checks the Tcl and Tk include and lib directories. Since @@ -669,21 +681,24 @@ if test $usingTcl ; then tmpstr=${TCL_LIB_SPEC#*-L} TCL_LIB_DIR=${tmpstr% -l*} - tmpstr=${TK_LIB_SPEC#*-L} - TK_LIB_DIR=${tmpstr% -l*} TCL_INC_DIR=${TCL_INCLUDE_SPEC#*-I} - TK_INC_DIR=${TK_INCLUDE_SPEC#*-I} + if test $usingTk ; then + tmpstr=${TK_LIB_SPEC#*-L} + TK_LIB_DIR=${tmpstr% -l*} + TK_INC_DIR=${TK_INCLUDE_SPEC#*-I} - if test "$TCL_VERSION" = "7.6" -a "$TK_VERSION" = "4.2" ; then - : - elif test "$TCL_VERSION" = "7.5" -a "$TK_VERSION" = "4.1" ; then - : - elif test "$TCL_VERSION" = "$TK_VERSION" ; then - : - else - echo "Mismatched Tcl/Tk versions ($TCL_VERSION != $TK_VERSION)" - echo "Reverting to non-Tcl compile" - usingTcl= + if test "$TCL_VERSION" = "7.6" -a "$TK_VERSION" = "4.2" ; then + : + elif test "$TCL_VERSION" = "7.5" -a "$TK_VERSION" = "4.1" ; then + : + elif test "$TCL_VERSION" = "$TK_VERSION" ; then + : + else + echo "Mismatched Tcl/Tk versions ($TCL_VERSION != $TK_VERSION)" + echo "Reverting to non-Tcl compile" + usingTcl= + usingTk= + fi fi fi @@ -716,14 +731,14 @@ if test $usingTcl ; then fi fi -if test $usingTcl ; then +if test $usingTk ; then if test "x${magic_with_tk_includes}" != "x" ; then if test -r "${magic_with_tk_includes}/tk.h" ; then TK_INC_DIR=${magic_with_tk_includes} else echo "Can't find tk.h in \"${magic_with_tk_includes}\"" - echo "Reverting to non-Tcl compile" - usingTcl= + echo "Reverting to non-Tk compile" + usingTk= fi else for dir in \ @@ -740,8 +755,8 @@ if test $usingTcl ; then done if test "x${TK_INC_DIR}" = "x" ; then echo "Can't find tk.h header file" - echo "Reverting to non-Tcl compile" - usingTcl= + echo "Reverting to non-Tk compile" + usingTk= fi fi fi @@ -764,14 +779,14 @@ if test $usingTcl ; then if test "x${TCL_LIB_SPEC}" = "x" ; then TCL_LIB_SPEC="-l${TCL_LIB_NAME}" fi - if test "x${TK_LIB_SPEC}" = "x" ; then + if test $usingTk -a "x${TK_LIB_SPEC}" = "x" ; then TK_LIB_SPEC="-l${TK_LIB_NAME}" fi # Find the version of "wish" that corresponds to TCL_EXEC_PREFIX # We really ought to run "ldd" to confirm that the linked libraries match. - if test "x${magic_with_wish_binary}" = "x" ; then + if test $usingTk -a "x${magic_with_wish_binary}" = "x" ; then AC_MSG_CHECKING([for wish executable]) for dir in \ ${TK_EXEC_PREFIX}/bin \ @@ -800,7 +815,7 @@ if test $usingTcl ; then else AC_MSG_RESULT([${WISH_EXE}]) fi - else + elif test $usingTk ; then WISH_EXE=${magic_with_wish_binary} fi @@ -890,7 +905,7 @@ if test $usingTcl ; then fi fi -if test $usingTcl ; then +if test $usingTk ; then if test "x${magic_with_tk_libraries}" != "x" ; then for libname in \ "${magic_with_tk_libraries}/${TCL_LIB_FILE}" \ @@ -905,8 +920,8 @@ if test $usingTcl ; then done if test "x${TK_LIB_DIR}" = "x" ; then echo "Can't find tk library in \"${magic_with_tk_libraries}\"" - echo "Reverting to non-Tcl compile" - usingTcl= + echo "Reverting to non-Tk compile" + usingTk= fi else for libname in \ @@ -921,8 +936,8 @@ if test $usingTcl ; then done if test "x${TK_LIB_DIR}" = "x" ; then echo "Can't find tk library" - echo "Reverting to non-Tcl compile" - usingTcl= + echo "Reverting to non-Tk compile" + usingTk= fi fi fi @@ -1356,7 +1371,12 @@ if test $usingTcl ; then AC_DEFINE(MAGIC_WRAPPER) extra_libs="$extra_libs \${MAGICDIR}/tcltk/libtcltk.o" extra_defs="$extra_defs -DTCL_DIR=\\\"\${TCLDIR}\\\"" - stub_defs="$stub_defs -DUSE_TCL_STUBS -DUSE_TK_STUBS" + if test $usingTk ; then + stub_defs="$stub_defs -DUSE_TCL_STUBS -DUSE_TK_STUBS" + else + stub_defs="$stub_defs -DUSE_TCL_STUBS -DMAGIC_NO_TK" + extra_defs="$extra_defs -DMAGIC_NO_TK" + fi elif test $usingScheme ; then modules="$modules lisp" unused="$unused tcltk" @@ -1401,7 +1421,9 @@ if test $usingTcl ; then gr_libs="$gr_libs -lX11" fi fi - gr_srcs="$gr_srcs \${TKCOMMON_SRCS}" + if test $usingTk ; then + gr_srcs="$gr_srcs \${TKCOMMON_SRCS}" + fi else if test $usingX11 ; then gr_dflags="$gr_dflags -DX11 -DXLIB" @@ -1506,23 +1528,25 @@ if test $usingTcl ; then # ----------------------------------------------------------------------- # -# Tk libraries and header files +# Tk libraries and header files (skipped under --without-tk) # # ----------------------------------------------------------------------- - if test "${TK_INC_DIR}" != "/usr/include" ; then - INC_SPECS="${INC_SPECS} -I${TK_INC_DIR}" - fi - if test "${TK_LIB_DIR}" = "/usr/lib" -o \ - "${TK_LIB_DIR}" = "/usr/lib64" ; then - LIB_SPECS_NOSTUB="${LIB_SPECS_NOSTUB} ${TK_LIB_SPEC}" - LIB_SPECS="${LIB_SPECS} ${TK_STUB_LIB_SPEC}" - else - LIB_SPECS_NOSTUB="${LIB_SPECS_NOSTUB} -L${TK_LIB_DIR} ${TK_LIB_SPEC}" - LIB_SPECS="${LIB_SPECS} -L${TK_LIB_DIR} ${TK_STUB_LIB_SPEC}" - if test "x${loader_run_path}" = "x" ; then - loader_run_path="${TK_LIB_DIR}" + if test $usingTk ; then + if test "${TK_INC_DIR}" != "/usr/include" ; then + INC_SPECS="${INC_SPECS} -I${TK_INC_DIR}" + fi + if test "${TK_LIB_DIR}" = "/usr/lib" -o \ + "${TK_LIB_DIR}" = "/usr/lib64" ; then + LIB_SPECS_NOSTUB="${LIB_SPECS_NOSTUB} ${TK_LIB_SPEC}" + LIB_SPECS="${LIB_SPECS} ${TK_STUB_LIB_SPEC}" else - loader_run_path="${TK_LIB_DIR}:${loader_run_path}" + LIB_SPECS_NOSTUB="${LIB_SPECS_NOSTUB} -L${TK_LIB_DIR} ${TK_LIB_SPEC}" + LIB_SPECS="${LIB_SPECS} -L${TK_LIB_DIR} ${TK_STUB_LIB_SPEC}" + if test "x${loader_run_path}" = "x" ; then + loader_run_path="${TK_LIB_DIR}" + else + loader_run_path="${TK_LIB_DIR}:${loader_run_path}" + fi fi fi diff --git a/tcltk/Makefile b/tcltk/Makefile index 7e29acd7..372ecdd7 100644 --- a/tcltk/Makefile +++ b/tcltk/Makefile @@ -41,6 +41,16 @@ BIN_FILES = \ tcl-main: magicexec magicdnull magic.tcl magic.sh ext2spice.sh ext2sim.sh +# In the WASM build the tcltk module ships only as libtcltk.o; there is no +# wish-like launcher binary, no magic.sh, etc. `make mains` iterates over +# PROGRAMS which now includes tcltk (because --with-tcl is on), so define a +# no-op `main` to keep that loop happy. The native build still uses tcl-main +# via `make all` → tcllibrary. +ifeq (${MAKE_WASM},1) +main: + @: +endif + install-tcl: magicexec magicdnull ${BIN_FILES} ${TCL_FILES} ${RM} $(DESTDIR)${INSTALL_TCLDIR}/magicexec ${CP} magicexec $(DESTDIR)${INSTALL_TCLDIR}/magicexec diff --git a/tcltk/tclmagic.c b/tcltk/tclmagic.c index dd1e3aec..b15302b9 100644 --- a/tcltk/tclmagic.c +++ b/tcltk/tclmagic.c @@ -178,8 +178,10 @@ TagCallback(interp, tkpath, argc, argv) windCheckOnlyWindow(&w, DBWclientID); if (w != NULL && !(w->w_flags & WIND_OFFSCREEN)) { +#ifndef MAGIC_NO_TK Tk_Window tkwind = (Tk_Window) w->w_grdata; if (tkwind != NULL) tkpath = Tk_PathName(tkwind); +#endif } } if (tkpath == NULL) @@ -742,8 +744,10 @@ _magic_initialize(ClientData clientData, /* (See graphics/grTkCommon.c) */ /* (Unless "-dnull" option has been given) */ +#ifndef MAGIC_NO_TK if (strcmp(MainDisplayType, "NULL")) RegisterTkCommands(interp); +#endif /* Set up the console so that its menu option File->Exit */ /* calls magic's exit routine first. This should not be */ diff --git a/tcltk/tclmagic.h b/tcltk/tclmagic.h index fb94dab4..bb8c3b31 100644 --- a/tcltk/tclmagic.h +++ b/tcltk/tclmagic.h @@ -12,7 +12,9 @@ #ifdef MAGIC_WRAPPER #include +#ifndef MAGIC_NO_TK #include +#endif /* Externally-defined global variables */ diff --git a/toolchains/emscripten/build-tcl-wasm.sh b/toolchains/emscripten/build-tcl-wasm.sh new file mode 100644 index 00000000..88c53944 --- /dev/null +++ b/toolchains/emscripten/build-tcl-wasm.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# Build intubun/tcl as a static WASM library for linking into magic.wasm. +# +# 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/} +# +# Usage: +# build-tcl-wasm.sh --src= [--out=] [--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 +# git checkout of intubun/tcl pointed to by --src. +# +# 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= 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 + echo " Reclone intubun/tcl with: git -c core.autocrlf=false clone …" >&2 + 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 /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/^/ /' diff --git a/toolchains/emscripten/defs.mak b/toolchains/emscripten/defs.mak index dcace819..fa3d032b 100644 --- a/toolchains/emscripten/defs.mak +++ b/toolchains/emscripten/defs.mak @@ -24,11 +24,12 @@ TOP_EXTRA_LIBS += \ -sWASM=1 \ -sMODULARIZE=1 \ -sEXPORT_ES6=1 \ + -sUSE_ZLIB=1 \ -sEXPORTED_FUNCTIONS=_magic_wasm_init,_magic_wasm_run_command,_magic_wasm_source_file,_magic_wasm_update \ -sEXPORTED_RUNTIME_METHODS=cwrap,ccall,FS,setValue,getValue \ -sALLOW_MEMORY_GROWTH=1 \ -sINITIAL_MEMORY=33554432 \ - -sSTACK_SIZE=5242880 \ + -Wl,-z,stack-size=5242880 \ -sASSERTIONS=1 \ -sENVIRONMENT=node,web,worker \ -sFORCE_FILESYSTEM=1 \