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.
This commit is contained in:
Enno Schnackenberg 2026-05-17 21:41:03 +02:00 committed by Intubun
parent a062fdcfe0
commit 6b3b2f5162
21 changed files with 708 additions and 208 deletions

View File

@ -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
# @<owner>/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<x.y.z>... 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 @<owner>/ 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<x.y.z>... 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 }}

1
.gitignore vendored
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -8,6 +8,10 @@
#include <stdio.h>
#include <stdlib.h>
#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

8
npm/.gitignore vendored
View File

@ -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

View File

@ -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=<tcl|notcl|both>] [--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

View File

@ -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');

View File

@ -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');

View File

@ -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 };

6
npm/notcl.js Normal file
View File

@ -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';

View File

@ -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"

33
npm/tcl.js Normal file
View File

@ -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;

16
npm/tcl.ref Normal file
View File

@ -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

112
scripts/configure vendored
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 */

View File

@ -12,7 +12,9 @@
#ifdef MAGIC_WRAPPER
#include <tcl.h>
#ifndef MAGIC_NO_TK
#include <tk.h>
#endif
/* Externally-defined global variables */

View File

@ -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/<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
# 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=<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
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 <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/^/ /'

View File

@ -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 \