#!/usr/bin/env bash # Compila o LambdaHack (motor de roguelike em Haskell de Andres Loeh e Mikolaj Konarski, BSD-3-Clause, com o jogo de exemplo # "LambdaHack") para o navegador, a partir do commit b0d3d43 de github.com/LambdaHack/LambdaHack, como o "make build-ts" do projeto # (o "build-wasm" mais o "deployPage" do Makefile): GHC 9.12.4 com backend wasm instalado por lambdahack/install-ghc-wasm.sh em # artifacts/tools/ghc-wasm-9.12 (ghc-wasm-meta pinado, hash conferido), cabal com o cabal.project.freeze versionado em lambdahack/ e # --index-state 2026-11-08T20:06:11Z, "wasm32-wasi-cabal build exe:LambdaHack --enable-optimization", post-link.mjs do GHC (JSFFI) # e o harness TypeScript de ts-src/ (npm ci com o package-lock do projeto, esbuild). # # O pretty-show precisa do "happy" nativo, que nao existe aqui: um script em artifacts/tools/ghc-wasm-9.12/happy-wrapper roda o # happy.wasm que o proprio cabal compila (happy 2.2) no wasmtime. O jogo nao muda; a pagina (a do projeto sem os links de icones e do # GitHub, com CSP e o fetch do .wasm.gz) sai de lambdahack/index.html.py. # # O binario .wasm leva o GMP (LGPL-3.0-or-later ou GPL-2.0-or-later) ligado estaticamente pelo GHC: o fonte do GMP 6.3.0 e dos pacotes # do Hackage (mais o fonte do projeto) ficam em "sources" da receita. # # src/FredGames.Bundler/Builds/build-lambdahack.sh # # Saida: artifacts/build/lambdahack-b0d3d43/{lambdahack-b0d3d43-dist.tar.gz,LambdaHack.wasm,licenses.txt,hackage-sources.tar}; dois builds # seguidos tem que dar os mesmos arquivos. (KEEP_TREE=1 reaproveita a arvore e o dist-newstyle de uma execucao anterior.) set -euo pipefail COMMIT=b0d3d43aedd2131a016acfd67f8c00fbfb95de3f SHA256=99959ea06b210a2ce71840b968b8a752156bf2d6f3de8517049faa4935623993 INDEX_STATE=2026-11-08T20:06:11Z GMP_URL=https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz GMP_SHA256=a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898 here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" root="$(git -C "$here" rev-parse --show-toplevel 2>/dev/null || pwd)" out="$root/artifacts/build/lambdahack-${COMMIT:0:7}" cache="$root/artifacts/build/cache" ghcwasm="$root/artifacts/tools/ghc-wasm-9.12" mkdir -p "$cache" fetch() { # url arquivo sha256 if [ ! -f "$cache/$2" ]; then curl -fsSL --retry 4 "$1" -o "$cache/$2.partial" && mv "$cache/$2.partial" "$cache/$2" fi echo "$3 $cache/$2" | shasum -a 256 -c - > /dev/null } fetch "https://github.com/LambdaHack/LambdaHack/archive/$COMMIT.tar.gz" "lambdahack-${COMMIT:0:7}.tar.gz" "$SHA256" fetch "$GMP_URL" gmp-6.3.0.tar.xz "$GMP_SHA256" "$here/lambdahack/install-ghc-wasm.sh" mkdir -p "$ghcwasm/happy-wrapper" cat > "$ghcwasm/happy-wrapper/happy" <<'WRAP' #!/bin/sh D="$(cd "$(dirname "$0")/.." && pwd)" H="$(ls -d "$D"/.cabal/store/ghc-*/hppy-2.2-*/bin/happy.wasm | head -1)" exec "$D/wasmtime/bin/wasmtime" run -C cache=n -C compiler=cranelift -C parallel-compilation=n --env PATH= --env PWD="$PWD" --dir /::/ -O opt-level=0 -- "$H" "$@" WRAP chmod +x "$ghcwasm/happy-wrapper/happy" tree="$out/tree" if [ -z "${KEEP_TREE:-}" ]; then rm -rf "$out"; mkdir -p "$tree" tar -xzf "$cache/lambdahack-${COMMIT:0:7}.tar.gz" -C "$tree" --strip-components 1 else mkdir -p "$tree" find "$out" -maxdepth 1 -type f -delete rm -rf "$out/dist" "$out/work" fi cp "$here/lambdahack/cabal.project.freeze" "$tree/cabal.project.freeze" mkdir -p "$out/dist" "$out/work" # 1. o motor e o jogo (o "build-wasm" do Makefile) ( cd "$tree" # shellcheck disable=SC1091 . "$ghcwasm/env" export PATH="$ghcwasm/happy-wrapper:$PATH" wasm32-wasi-cabal build exe:LambdaHack --enable-optimization --index-state="$INDEX_STATE" > "$out/cabal-build.log" 2>&1 \ || { tail -40 "$out/cabal-build.log"; exit 1; } wasm=$(wasm32-wasi-cabal list-bin exe:LambdaHack --enable-optimization --index-state="$INDEX_STATE") cp "$wasm" "$out/work/LambdaHack.wasm" # 2. o harness TypeScript e o JSFFI (o "deployPage") export npm_config_cache="$cache/npm-lambdahack" ( cd ts-src && npm ci > "$out/npm-ci.log" 2>&1 && BUILD_OUTDIR="$out/work" npm run build >> "$out/npm-ci.log" 2>&1 ) \ || { tail -20 "$out/npm-ci.log"; exit 1; } node "$ghcwasm/wasm32-wasi-ghc/lib/post-link.mjs" --input "$wasm" --output "$out/dist/ghc_wasm_jsffi.mjs" cp ts-src/node_modules/lz-string/libs/lz-string.min.js "$out/dist/" cp GameDefinition/fonts/16x16xw.woff GameDefinition/fonts/*.ttf.woff "$out/dist/" ) cp "$out/work/bundle.js" "$out/dist/bundle.js" python3 -I "$here/lambdahack/index.html.py" "$tree/GameDefinition/index.html" "$out/dist/index.html" gzip -n -9 -c "$out/work/LambdaHack.wasm" > "$out/work/LambdaHack.wasm.gz" cp "$out/work/LambdaHack.wasm" "$out/LambdaHack.wasm" # 3. fonte dos pacotes do Hackage que entram no .wasm (tarballs que o cabal baixou, conferidos pelo indice) e licencas python3 -I - "$tree" "$ghcwasm" "$out" <<'PY' import json, os, sys, tarfile, io, glob tree, ghcwasm, out = sys.argv[1:] plan = json.load(open(os.path.join(tree, 'dist-newstyle/cache/plan.json'))) byid = {p['id']: p for p in plan['install-plan']} start = [p['id'] for p in plan['install-plan'] if p.get('pkg-name') == 'LambdaHack' and p.get('component-name') == 'exe:LambdaHack'] assert len(start) == 1, start seen, stack = set(), list(start) while stack: # so o que o executavel usa (sem os pacotes so dos testes) i = stack.pop() if i in seen or i not in byid: continue seen.add(i) p = byid[i] stack.extend(p.get('depends', [])) for c in p.get('components', {}).values(): stack.extend(c.get('depends', [])) used = [byid[i] for i in seen] pkgs = sorted({(p['pkg-name'], p['pkg-version']) for p in used if p['type'] == 'configured' and p.get('style') == 'global'}) pre = sorted({(p['pkg-name'], p['pkg-version']) for p in used if p['type'] == 'pre-existing'}) base = os.path.join(ghcwasm, '.cabal/packages/hackage.haskell.org') with tarfile.open(os.path.join(out, 'hackage-sources.tar'), 'w', format=tarfile.USTAR_FORMAT) as t: lic = [] for n, v in pkgs: f = os.path.join(base, n, v, f'{n}-{v}.tar.gz') data = open(f, 'rb').read() ti = tarfile.TarInfo(f'{n}-{v}.tar.gz'); ti.size = len(data); ti.mtime = 0; ti.mode = 0o644 t.addfile(ti, io.BytesIO(data)) with tarfile.open(f) as src: for m in src.getmembers(): b = os.path.basename(m.name).upper() if m.isfile() and m.name.count('/') == 1 and b.startswith(('LICENSE', 'LICENCE', 'COPYING', 'UNLICENSE', 'NOTICE')): lic.append(f'=== {n}-{v}: {os.path.basename(m.name)}\n' + src.extractfile(m).read().decode('utf-8', 'replace').rstrip() + '\n') if not any(l.startswith(f'=== {n}-{v}:') for l in lic): lic.append(f'=== {n}-{v}: (no licence file in the package; see its .cabal)\n') open(os.path.join(out, 'work/hackage-licenses.txt'), 'w').write('\n'.join(lic)) open(os.path.join(out, 'work/ghc-boot-packages.txt'), 'w').write('\n'.join(f'{n}-{v}' for n, v in pre) + '\n') PY { echo "== LambdaHack: LICENSE (BSD-3-Clause, original text)"; cat "$tree/LICENSE" echo; echo "== LambdaHack: COPYLEFT (licences of the parts, original text)"; cat "$tree/COPYLEFT" echo; echo "== LambdaHack: CREDITS"; cat "$tree/CREDITS" echo; echo "== GMP 6.3.0 (linked statically into LambdaHack.wasm by GHC): dual licensed LGPL-3.0-or-later OR GPL-2.0-or-later; its source is hosted next to this build" for f in COPYING COPYINGv2 COPYING.LESSERv3 COPYINGv3; do echo; echo "--- gmp-6.3.0/$f"; tar -xJOf "$cache/gmp-6.3.0.tar.xz" "gmp-6.3.0/$f"; done echo; echo "== GHC boot libraries linked into the game (BSD-3-Clause, see the GHC distribution); packages:"; cat "$out/work/ghc-boot-packages.txt" echo; echo "== Hackage packages (licence files of each package, from its tarball; the tarballs are hosted next to this build)" cat "$out/work/hackage-licenses.txt" } > "$out/licenses.txt" find "$out/dist" -exec touch -t 202611080000 {} + ( cd "$out" && find dist -type f | LC_ALL=C sort > files.txt \ && COPYFILE_DISABLE=1 tar --format=ustar --uid 0 --gid 0 --uname root --gname root -n -cf - -T files.txt \ | gzip -n -9 > "lambdahack-${COMMIT:0:7}-dist.tar.gz" && rm files.txt ) ls -l "$out/lambdahack-${COMMIT:0:7}-dist.tar.gz" "$out/LambdaHack.wasm" "$out/hackage-sources.tar" shasum -a 256 "$out/lambdahack-${COMMIT:0:7}-dist.tar.gz" "$out/LambdaHack.wasm" "$out/licenses.txt" "$out/hackage-sources.tar" [ -n "${KEEP_TREE:-}" ] || rm -rf "$tree"