"""Build the web Studio: the Studio GUI with the isokit engine compiled to WASM. python tools/build_web.py [--out dist/web] [--skip-cargo] Needs the wasm32-unknown-unknown target (rustup target add wasm32-unknown-unknown) and the wasm-bindgen CLI at the version in Cargo.lock. wasm-opt (binaryen) is used when it is on PATH. The output folder is static files: serve it from any web server (it needs http(s), not file://). """ import argparse import hashlib import json import os import re import shutil import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent SKIP_DIRS = {"dist", "target", "node_modules"} def run(cmd, **kw): print("+", " ".join(str(c) for c in cmd), flush=True) subprocess.run([str(c) for c in cmd], check=True, cwd=ROOT, **kw) def lock_version(name): lock = (ROOT / "Cargo.lock").read_text(encoding="utf-8") m = re.search(r'name = "%s"\nversion = "([^"]+)"' % re.escape(name), lock) return m.group(1) if m else None def demo_files(): """The demo project as the web Studio stores projects: path -> text.""" base = ROOT / "examples" / "demo" out = {} for p in sorted(base.rglob("*")): rel = p.relative_to(base).as_posix() parts = rel.split("/") if p.is_dir() or any(s.startswith(".") or s in SKIP_DIRS for s in parts[:-1]): continue if p.suffix in (".rhai", ".toml", ".json") or p.name == "README.md": out[rel] = p.read_text(encoding="utf-8") assert "isokit.toml" in out, "examples/demo has no isokit.toml" return out def replace_once(s, old, new): n = s.count(old) assert n == 1, f"studio/index.html: expected {old!r} once, found {n}" return s.replace(old, new) def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--out", default=str(ROOT / "dist" / "web")) ap.add_argument("--skip-cargo", action="store_true", help="reuse the last WASM build (front-end changes only)") a = ap.parse_args() out = Path(a.out).resolve() web = out / "assets" / "web" wasm = ROOT / "target" / "wasm32-unknown-unknown" / "web" / "isokit_wasm.wasm" if not a.skip_cargo: want = lock_version("wasm-bindgen") try: have = subprocess.run(["wasm-bindgen", "--version"], capture_output=True, text=True, check=True).stdout.split()[-1] except (OSError, subprocess.CalledProcessError): sys.exit(f"wasm-bindgen CLI not found: cargo install wasm-bindgen-cli --version {want}") if have != want: sys.exit(f"wasm-bindgen CLI is {have}, Cargo.lock has {want}: cargo install wasm-bindgen-cli --version {want}") run(["cargo", "build", "-p", "isokit-wasm", "--target", "wasm32-unknown-unknown", "--profile", "web"]) if out.exists(): shutil.rmtree(out) web.mkdir(parents=True) if not wasm.exists(): sys.exit(f"{wasm} missing: run without --skip-cargo") run(["wasm-bindgen", "--target", "web", "--no-typescript", "--remove-name-section", "--remove-producers-section", "--out-dir", web.as_posix(), wasm.as_posix()]) bg = web / "isokit_wasm_bg.wasm" if shutil.which("wasm-opt"): run(["wasm-opt", "-Oz", "--all-features", bg.as_posix(), "-o", bg.as_posix()]) else: print(" (wasm-opt not found: the .wasm stays unoptimised by binaryen, about 15% larger)") for src, dst in [("viewer/isokit-view.js", "viewer"), ("viewer/isokit-view.css", "viewer"), ("studio/studio.js", "studio"), ("studio/studio.css", "studio"), ("web/web.js", "web"), ("web/web.css", "web")]: (out / "assets" / dst).mkdir(parents=True, exist_ok=True) shutil.copy2(ROOT / src, out / "assets" / dst / Path(src).name) (web / "demo.json").write_text(json.dumps(demo_files(), ensure_ascii=False), encoding="utf-8") h = hashlib.sha256() for p in sorted((out / "assets").rglob("*")): if p.is_file(): h.update(p.read_bytes()) v = "v=" + h.hexdigest()[:10] html = (ROOT / "studio" / "index.html").read_text(encoding="utf-8") html = replace_once(html, 'isokit Studio', 'isokit Studio\n') html = replace_once(html, 'href="/assets/viewer/isokit-view.css"', f'href="assets/viewer/isokit-view.css?{v}"') html = replace_once(html, '', '') html = replace_once(html, '', f'\n') html = replace_once(html, 'href="/view"', 'href="#"') html = replace_once(html, '', f'') html = replace_once(html, '', f'\n' '') assert "/assets/" not in html, "an absolute /assets/ URL is left in index.html" (out / "index.html").write_text(html, encoding="utf-8") total = 0 for p in sorted(out.rglob("*")): if p.is_file(): total += p.stat().st_size print(f" {p.relative_to(out).as_posix():40} {p.stat().st_size / 1024:8.1f} kB") print(f"{out} ({total / 1024 / 1024:.1f} MB, {v})") if __name__ == "__main__": main()