#!/usr/bin/env bash # Build Magic WASM and copy artifacts into this npm/ directory. # # Usage: # npm/build.sh [--variant=] [--release] [--test] [--pack] # # --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). # # Requirements (must be on PATH or set via env vars before running): # emcc / emmake / emconfigure — Emscripten compiler tools # make, gcc — standard build tools # node, npm — only required for --test / --pack # # Environment: # 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 tcltk/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 tcltk/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 ;; --variant=tcl) OPT_VARIANT=tcl ;; --variant=notcl) OPT_VARIANT=notcl ;; --variant=both) OPT_VARIANT=both ;; *) echo "Unknown option: $arg" >&2; exit 1 ;; esac done # --- locate emscripten ------------------------------------------------------- if [ -n "${EMSDK_DIR:-}" ]; then if [ ! -f "$EMSDK_DIR/emsdk_env.sh" ]; then echo "Error: EMSDK_DIR is set to '$EMSDK_DIR' but emsdk_env.sh was not found there." >&2 exit 1 fi # shellcheck source=/dev/null source "$EMSDK_DIR/emsdk_env.sh" else if ! command -v emcc &>/dev/null; then echo "Error: emcc not found on PATH and EMSDK_DIR is not set." >&2 echo " Either source emsdk_env.sh before running this script," >&2 echo " or set EMSDK_DIR to your emsdk checkout directory." >&2 exit 1 fi fi echo "Using emcc: $(command -v emcc)" emcc --version | head -1 # --- portability helpers ----------------------------------------------------- # CPU count: Linux has nproc, macOS has sysctl, fall back to getconf. ncpu() { if command -v nproc &>/dev/null; then nproc elif [ "$(uname)" = "Darwin" ]; then sysctl -n hw.ncpu else getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1 fi } # Portable in-place sed (BSD sed on macOS disagrees with GNU on -i). # Uses redirect-back instead of mv so the file's mode bits are preserved # (configure must stay executable across build.sh invocations). sed_strip_cr() { local file=$1 tmp tmp=$(mktemp) sed 's/\r//' "$file" > "$tmp" && cat "$tmp" > "$file" && rm "$tmp" } if [ $OPT_RELEASE -eq 1 ]; then EXTRA_CFLAGS="-O2" else EXTRA_CFLAGS="-g" fi # --- TCL fork: locate and prebuild (TCL variant only) ----------------------- # Uses TCL_REPO_URL and TCL_REF from the environment (both have defaults). # If the TCL source tree does not exist yet, clones it. If it does exist, # checks out the requested ref — no auto-fetch, so builds are 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() { : "${TCL_REPO_URL:=https://github.com/tcltk/tcl.git}" : "${TCL_REF:=main}" 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 ( 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)" ) # 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-wasm.yml). if [ $OPT_TEST -eq 1 ]; then cd "$SCRIPT_DIR" npm run test npm run test:tcl fi # --- optional pack ----------------------------------------------------------- if [ $OPT_PACK -eq 1 ]; then "$SCRIPT_DIR/pack.sh" echo "npm package tarball created in npm/" fi