#!/usr/bin/env bash # Reproducible WebAssembly build of liblouis for uebtranslator.com. # # Inputs (all pinned): liblouis release tarball (SHA-256 in liblouis-*.tar.gz.sha256), # Emscripten SDK version, emcc-flags.txt, tables.list. # Outputs (engine/out): liblouis.mjs, liblouis..wasm, tables/*, manifest.json. # # The library and tables are built and shipped UNMODIFIED. Nothing from liblouis-js (GPL-3.0) # is used: no --pre-js/--post-js, our glue lives in src/lib/engine/ under MIT. # Run only when the pinned version changes: npm run build-engine set -euo pipefail LIBLOUIS_VERSION="3.39.0" EMSDK_VERSION="6.0.10" PYTHON_RELEASE="20260924" PYTHON_VERSION="3.12.14" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SRC_DIR="$HERE/src" # OUT_DIR and BUILD_DIR may be overridden: scripts/verify-engine-build.mjs unpacks, compiles and # links in a temporary directory and compares hashes with the committed engine/out. The defaults # are the committed output and engine/src (where the verified tarball is cached). OUT_DIR="${OUT_DIR:-$HERE/out}" BUILD_DIR="${BUILD_DIR:-$SRC_DIR}" TARBALL="liblouis-${LIBLOUIS_VERSION}.tar.gz" TARBALL_URL="https://github.com/liblouis/liblouis/releases/download/v${LIBLOUIS_VERSION}/${TARBALL}" LIB_DIR="$BUILD_DIR/liblouis-${LIBLOUIS_VERSION}" log() { printf '\033[1m[engine]\033[0m %s\n' "$*"; } die() { printf '\033[31m[engine] %s\033[0m\n' "$*" >&2; exit 1; } sha256() { shasum -a 256 "$1" | awk '{print $1}'; } # --------------------------------------------------------------------------- # 1. Source tarball, verified against the pinned hash # --------------------------------------------------------------------------- mkdir -p "$SRC_DIR" if [[ ! -f "$SRC_DIR/$TARBALL" ]]; then log "downloading $TARBALL" curl -fsSL -o "$SRC_DIR/$TARBALL" "$TARBALL_URL" fi (cd "$SRC_DIR" && shasum -a 256 -c "$HERE/${TARBALL}.sha256") || die "tarball hash mismatch — refusing to build" TARBALL_SHA="$(sha256 "$SRC_DIR/$TARBALL")" rm -rf "$LIB_DIR" mkdir -p "$BUILD_DIR" tar xzf "$SRC_DIR/$TARBALL" -C "$BUILD_DIR" # --------------------------------------------------------------------------- # 2. Toolchain: Python >= 3.10 (emsdk requirement) and the pinned Emscripten SDK, # both installed inside engine/ (gitignored), never system-wide # --------------------------------------------------------------------------- python_ok() { "$1" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)' 2>/dev/null; } PY="" if command -v python3 >/dev/null && python_ok python3; then PY="$(command -v python3)" elif [[ -x "$HERE/.python/python/bin/python3" ]]; then PY="$HERE/.python/python/bin/python3" else case "$(uname -s)-$(uname -m)" in Darwin-arm64) TRIPLE="aarch64-apple-darwin" ;; Darwin-x86_64) TRIPLE="x86_64-apple-darwin" ;; Linux-x86_64) TRIPLE="x86_64-unknown-linux-gnu" ;; Linux-aarch64) TRIPLE="aarch64-unknown-linux-gnu" ;; *) die "no Python >= 3.10 and no standalone build for $(uname -s)-$(uname -m)" ;; esac log "fetching standalone CPython ${PYTHON_VERSION} (${TRIPLE})" mkdir -p "$HERE/.python" curl -fsSL "https://github.com/astral-sh/python-build-standalone/releases/download/${PYTHON_RELEASE}/cpython-${PYTHON_VERSION}%2B${PYTHON_RELEASE}-${TRIPLE}-install_only.tar.gz" \ | tar xz -C "$HERE/.python" PY="$HERE/.python/python/bin/python3" fi export EMSDK_PYTHON="$PY" if [[ ! -d "$HERE/.emsdk" ]]; then log "cloning emsdk ${EMSDK_VERSION}" git clone --quiet --depth 1 --branch "$EMSDK_VERSION" https://github.com/emscripten-core/emsdk.git "$HERE/.emsdk" fi if [[ ! -x "$HERE/.emsdk/upstream/emscripten/emcc" ]]; then log "installing Emscripten ${EMSDK_VERSION}" (cd "$HERE/.emsdk" && ./emsdk install "$EMSDK_VERSION" && ./emsdk activate "$EMSDK_VERSION") fi # shellcheck disable=SC1091 source "$HERE/.emsdk/emsdk_env.sh" >/dev/null 2>&1 EMCC_VERSION="$(emcc --version | head -1)" log "$EMCC_VERSION" # --------------------------------------------------------------------------- # 3. Compile the library (UTF-32 widechar so every code point is one unit) # --------------------------------------------------------------------------- cd "$LIB_DIR" log "configure (--host=wasm32-unknown-emscripten --enable-ucs4 --disable-shared)" emconfigure ./configure --host=wasm32-unknown-emscripten --build="$(./build-aux/config.guess)" --enable-ucs4 --disable-shared --without-yaml --quiet >/dev/null log "make liblouis.la" emmake make -C gnulib --quiet >/dev/null emmake make -C liblouis liblouis.la --quiet >/dev/null # --------------------------------------------------------------------------- # 4. Link to an ES module + separate .wasm # --------------------------------------------------------------------------- rm -rf "$OUT_DIR" mkdir -p "$OUT_DIR/tables" FLAGS=() while read -r f; do [[ -n "$f" ]] && FLAGS+=("$f"); done < "$HERE/emcc-flags.txt" log "link: ${FLAGS[*]}" emcc liblouis/.libs/liblouis.a "${FLAGS[@]}" -o "$OUT_DIR/liblouis.mjs" WASM_SHA="$(sha256 "$OUT_DIR/liblouis.wasm")" WASM_NAME="liblouis.${WASM_SHA:0:12}.wasm" mv "$OUT_DIR/liblouis.wasm" "$OUT_DIR/$WASM_NAME" MJS_SHA="$(sha256 "$OUT_DIR/liblouis.mjs")" # --------------------------------------------------------------------------- # 5. Tables: walk the include chain from tables.list; copy unmodified # --------------------------------------------------------------------------- # bash 3.2 compatible (macOS): SEEN is a newline-separated list SEEN="" QUEUE=() while read -r t; do [[ -n "$t" && "$t" != \#* ]] && QUEUE+=("$t"); done < "$HERE/tables.list" while ((${#QUEUE[@]})); do t="${QUEUE[0]}"; QUEUE=("${QUEUE[@]:1}") if printf '%s\n' "$SEEN" | grep -qxF "$t"; then continue; fi [[ -f "tables/$t" ]] || die "table include chain broken: tables/$t is missing" SEEN="$SEEN$t"$'\n' cp "tables/$t" "$OUT_DIR/tables/$t" while read -r inc; do [[ -n "$inc" ]] && QUEUE+=("$inc"); done < <(awk '$1 == "include" { print $2 }' "tables/$t") done TABLES="$(printf '%s' "$SEEN" | sort)" log "tables: $(echo $TABLES)" # --------------------------------------------------------------------------- # 6. Manifest (read by EngineStatus, the worker and validate-data) # --------------------------------------------------------------------------- gz() { gzip -9 -c "$1" | wc -c | tr -d ' '; } bytes() { wc -c < "$1" | tr -d ' '; } { printf '{\n' printf ' "liblouis": "%s",\n' "$LIBLOUIS_VERSION" printf ' "tarball": { "name": "%s", "sha256": "%s", "url": "%s" },\n' "$TARBALL" "$TARBALL_SHA" "$TARBALL_URL" printf ' "emscripten": "%s",\n' "$EMSDK_VERSION" printf ' "widechar": 4,\n' printf ' "module": { "name": "liblouis.mjs", "sha256": "%s", "bytes": %s, "gzip": %s },\n' "$MJS_SHA" "$(bytes "$OUT_DIR/liblouis.mjs")" "$(gz "$OUT_DIR/liblouis.mjs")" printf ' "wasm": { "name": "%s", "sha256": "%s", "bytes": %s, "gzip": %s },\n' "$WASM_NAME" "$WASM_SHA" "$(bytes "$OUT_DIR/$WASM_NAME")" "$(gz "$OUT_DIR/$WASM_NAME")" printf ' "tables": [\n' first=1 for t in $TABLES; do [[ $first -eq 1 ]] || printf ',\n' first=0 printf ' { "name": "%s", "sha256": "%s", "bytes": %s, "gzip": %s }' "$t" "$(sha256 "$OUT_DIR/tables/$t")" "$(bytes "$OUT_DIR/tables/$t")" "$(gz "$OUT_DIR/tables/$t")" done printf '\n ]\n}\n' } > "$OUT_DIR/manifest.json" cp "$HERE/LICENSE-liblouis" "$OUT_DIR/LICENSE" log "done → $OUT_DIR" cat "$OUT_DIR/manifest.json"