diff --git a/Makefile.pre.in b/Makefile.pre.in index 120a6add38507f..5ea00537629de0 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -1103,7 +1103,7 @@ $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS) # wasm32-emscripten browser web example -EMSCRIPTEN_DIR=$(srcdir)/Tools/wasm/emscripten +EMSCRIPTEN_DIR=$(srcdir)/Platforms/emscripten WEBEX_DIR=$(EMSCRIPTEN_DIR)/web_example/ ZIP_STDLIB=python$(VERSION)$(ABI_THREAD).zip @@ -3174,7 +3174,7 @@ Python/emscripten_trampoline_inner.wasm: $(srcdir)/Python/emscripten_trampoline_ $$(dirname $$(dirname $(CC)))/bin/clang -o $@ $< -mgc -O2 -Wl,--no-entry -Wl,--import-table -Wl,--import-memory -target wasm32-unknown-unknown -nostdlib Python/emscripten_trampoline_wasm.c: Python/emscripten_trampoline_inner.wasm - $(PYTHON_FOR_REGEN) $(srcdir)/Tools/wasm/emscripten/prepare_external_wasm.py $< $@ getWasmTrampolineModule + $(PYTHON_FOR_REGEN) $(srcdir)/Platforms/emscripten/prepare_external_wasm.py $< $@ getWasmTrampolineModule JIT_DEPS = \ $(srcdir)/Tools/jit/*.c \ diff --git a/Tools/wasm/README.md b/Platforms/emscripten/README.md similarity index 99% rename from Tools/wasm/README.md rename to Platforms/emscripten/README.md index 46228a5212a315..017bb3c8977d26 100644 --- a/Tools/wasm/README.md +++ b/Platforms/emscripten/README.md @@ -35,7 +35,7 @@ After building, you can run the full test suite with: ``` You can run the browser smoke test with: ```shell -./Tools/wasm/emscripten/browser_test/run_test.sh +./Platforms/emscripten/browser_test/run_test.sh ``` ### The Web Example diff --git a/Platforms/emscripten/__main__.py b/Platforms/emscripten/__main__.py new file mode 100644 index 00000000000000..40c0922bc54941 --- /dev/null +++ b/Platforms/emscripten/__main__.py @@ -0,0 +1,729 @@ +#!/usr/bin/env python3 + +import argparse +import contextlib +import functools +import hashlib +import json +import os +import shutil +import subprocess +import sys +import sysconfig +import tempfile +from pathlib import Path +from textwrap import dedent +from urllib.request import urlopen + +import tomllib + +try: + from os import process_cpu_count as cpu_count +except ImportError: + from os import cpu_count + + +EMSCRIPTEN_DIR = Path(__file__).parent +CHECKOUT = EMSCRIPTEN_DIR.parent.parent +CONFIG_FILE = EMSCRIPTEN_DIR / "config.toml" + +DEFAULT_CROSS_BUILD_DIR = CHECKOUT / "cross-build" +HOST_TRIPLE = "wasm32-emscripten" + + +@functools.cache +def load_config_toml(): + with CONFIG_FILE.open("rb") as file: + return tomllib.load(file) + + +@functools.cache +def required_emscripten_version(): + return load_config_toml()["emscripten-version"] + + +@functools.cache +def emsdk_cache_root(emsdk_cache): + required_version = required_emscripten_version() + return Path(emsdk_cache).absolute() / required_version + + +@functools.cache +def emsdk_activate_path(emsdk_cache): + return emsdk_cache_root(emsdk_cache) / "emsdk/emsdk_env.sh" + + +def get_build_paths(cross_build_dir=None, emsdk_cache=None): + """Compute all build paths from the given cross-build directory.""" + if cross_build_dir is None: + cross_build_dir = DEFAULT_CROSS_BUILD_DIR + cross_build_dir = Path(cross_build_dir).absolute() + host_triple_dir = cross_build_dir / HOST_TRIPLE + prefix_dir = host_triple_dir / "prefix" + if emsdk_cache: + prefix_dir = emsdk_cache_root(emsdk_cache) / "prefix" + + return { + "cross_build_dir": cross_build_dir, + "native_build_dir": cross_build_dir / "build", + "host_triple_dir": host_triple_dir, + "host_build_dir": host_triple_dir / "build", + "host_dir": host_triple_dir / "build" / "python", + "prefix_dir": prefix_dir, + } + + +LOCAL_SETUP = CHECKOUT / "Modules" / "Setup.local" +LOCAL_SETUP_MARKER = b"# Generated by Platforms/wasm/emscripten.py\n" + + +def validate_emsdk_version(emsdk_cache): + """Validate that the emsdk cache contains the required emscripten version.""" + required_version = required_emscripten_version() + emsdk_env = emsdk_activate_path(emsdk_cache) + if not emsdk_env.is_file(): + print( + f"Required emscripten version {required_version} not found in {emsdk_cache}", + file=sys.stderr, + ) + sys.exit(1) + print(f"โœ… Emscripten version {required_version} found in {emsdk_cache}") + + +def parse_env(text): + result = {} + for line in text.splitlines(): + key, val = line.split("=", 1) + result[key] = val + return result + + +@functools.cache +def get_emsdk_environ(emsdk_cache): + """Returns os.environ updated by sourcing emsdk_env.sh""" + if not emsdk_cache: + return os.environ + env_text = subprocess.check_output( + [ + "bash", + "-c", + f"EMSDK_QUIET=1 source {emsdk_activate_path(emsdk_cache)} && env", + ], + text=True, + ) + return parse_env(env_text) + + +def updated_env(updates, emsdk_cache): + """Create a new dict representing the environment to use. + + The changes made to the execution environment are printed out. + """ + env_defaults = {} + # https://reproducible-builds.org/docs/source-date-epoch/ + git_epoch_cmd = ["git", "log", "-1", "--pretty=%ct"] + try: + epoch = subprocess.check_output( + git_epoch_cmd, encoding="utf-8" + ).strip() + env_defaults["SOURCE_DATE_EPOCH"] = epoch + except subprocess.CalledProcessError: + pass # Might be building from a tarball. + # This layering lets SOURCE_DATE_EPOCH from os.environ takes precedence. + environment = env_defaults | get_emsdk_environ(emsdk_cache) | updates + env_diff = {} + for key, value in environment.items(): + if os.environ.get(key) != value: + env_diff[key] = value + + print("๐ŸŒŽ Environment changes:") + for key in sorted(env_diff.keys()): + print(f" {key}={env_diff[key]}") + + return environment + + +def subdir(path_key, *, clean_ok=False): + """Decorator to change to a working directory. + + path_key is a key into context.build_paths, used to resolve the working + directory at call time. + """ + + def decorator(func): + @functools.wraps(func) + def wrapper(context): + working_dir = context.build_paths[path_key] + try: + tput_output = subprocess.check_output( + ["tput", "cols"], encoding="utf-8" + ) + terminal_width = int(tput_output.strip()) + except subprocess.CalledProcessError: + terminal_width = 80 + print("โŽฏ" * terminal_width) + print("๐Ÿ“", working_dir) + if ( + clean_ok + and getattr(context, "clean", False) + and working_dir.exists() + ): + print("๐Ÿšฎ Deleting directory (--clean)...") + shutil.rmtree(working_dir) + + working_dir.mkdir(parents=True, exist_ok=True) + + with contextlib.chdir(working_dir): + return func(context, working_dir) + + return wrapper + + return decorator + + +def call(command, *, quiet, **kwargs): + """Execute a command. + + If 'quiet' is true, then redirect stdout and stderr to a temporary file. + """ + print("โฏ", " ".join(map(str, command))) + if not quiet: + stdout = None + stderr = None + else: + stdout = tempfile.NamedTemporaryFile( + "w", + encoding="utf-8", + delete=False, + prefix="cpython-emscripten-", + suffix=".log", + ) + stderr = subprocess.STDOUT + print(f"๐Ÿ“ Logging output to {stdout.name} (--quiet)...") + + subprocess.check_call(command, **kwargs, stdout=stdout, stderr=stderr) + + +def build_platform(): + """The name of the build/host platform.""" + # Can also be found via `config.guess`.` + return sysconfig.get_config_var("BUILD_GNU_TYPE") + + +def build_python_path(context): + """The path to the build Python binary.""" + native_build_dir = context.build_paths["native_build_dir"] + binary = native_build_dir / "python" + if not binary.is_file(): + binary = binary.with_suffix(".exe") + if not binary.is_file(): + raise FileNotFoundError( + f"Unable to find `python(.exe)` in {native_build_dir}" + ) + + return binary + + +def install_emscripten(context): + emsdk_cache = context.emsdk_cache + if emsdk_cache is None: + print("install-emscripten requires --emsdk-cache", file=sys.stderr) + sys.exit(1) + version = required_emscripten_version() + emsdk_target = emsdk_cache_root(emsdk_cache) / "emsdk" + if emsdk_target.exists(): + if not context.quiet: + print(f"Emscripten version {version} already installed") + return + if not context.quiet: + print(f"Installing emscripten version {version}") + emsdk_target.mkdir(parents=True) + call( + [ + "git", + "clone", + "https://github.com/emscripten-core/emsdk.git", + emsdk_target, + ], + quiet=context.quiet, + ) + call([emsdk_target / "emsdk", "install", version], quiet=context.quiet) + call([emsdk_target / "emsdk", "activate", version], quiet=context.quiet) + if not context.quiet: + print(f"Installed emscripten version {version}") + + +@subdir("native_build_dir", clean_ok=True) +def configure_build_python(context, working_dir): + """Configure the build/host Python.""" + if LOCAL_SETUP.exists(): + print(f"๐Ÿ‘ {LOCAL_SETUP} exists ...") + else: + print(f"๐Ÿ“ Touching {LOCAL_SETUP} ...") + LOCAL_SETUP.write_bytes(LOCAL_SETUP_MARKER) + + configure = [os.path.relpath(CHECKOUT / "configure", working_dir)] + if context.args: + configure.extend(context.args) + + call(configure, quiet=context.quiet) + + +@subdir("native_build_dir") +def make_build_python(context, working_dir): + """Make/build the build Python.""" + call(["make", "--jobs", str(cpu_count()), "all"], quiet=context.quiet) + + binary = build_python_path(context) + cmd = [ + binary, + "-c", + "import sys; " + "print(f'{sys.version_info.major}.{sys.version_info.minor}')", + ] + version = subprocess.check_output(cmd, encoding="utf-8").strip() + + print(f"๐ŸŽ‰ {binary} {version}") + + +def check_shasum(file: str, expected_shasum: str): + with open(file, "rb") as f: + digest = hashlib.file_digest(f, "sha256") + if digest.hexdigest() != expected_shasum: + raise RuntimeError(f"Unexpected shasum for {file}") + + +def download_and_unpack(working_dir: Path, url: str, expected_shasum: str): + with tempfile.NamedTemporaryFile( + suffix=".tar.gz", delete_on_close=False + ) as tmp_file: + with urlopen(url) as response: + shutil.copyfileobj(response, tmp_file) + tmp_file.close() + check_shasum(tmp_file.name, expected_shasum) + shutil.unpack_archive(tmp_file.name, working_dir) + + +def should_build_library(prefix, name, config, quiet): + cached_config = prefix / (name + ".json") + if not cached_config.exists(): + if not quiet: + print( + f"No cached build of {name} version {config['version']} found, building" + ) + return True + + try: + with cached_config.open("rb") as f: + cached_config = json.load(f) + except json.JSONDecodeError: + if not quiet: + print(f"Cached data for {name} invalid, rebuilding") + return True + if config == cached_config: + if not quiet: + print( + f"Found cached build of {name} version {config['version']}, not rebuilding" + ) + return False + + if not quiet: + print( + f"Found cached build of {name} version {config['version']} but it's out of date, rebuilding" + ) + return True + + +def write_library_config(prefix, name, config, quiet): + cached_config = prefix / (name + ".json") + with cached_config.open("w") as f: + json.dump(config, f) + if not quiet: + print(f"Succeded building {name}, wrote config to {cached_config}") + + +@subdir("host_build_dir", clean_ok=True) +def make_emscripten_libffi(context, working_dir): + prefix = context.build_paths["prefix_dir"] + libffi_config = load_config_toml()["libffi"] + if not should_build_library( + prefix, "libffi", libffi_config, context.quiet + ): + return + url = libffi_config["url"] + version = libffi_config["version"] + shasum = libffi_config["shasum"] + libffi_dir = working_dir / f"libffi-{version}" + shutil.rmtree(libffi_dir, ignore_errors=True) + download_and_unpack( + working_dir, + url.format(version=version), + shasum, + ) + call( + [EMSCRIPTEN_DIR / "make_libffi.sh"], + env=updated_env({"PREFIX": prefix}, context.emsdk_cache), + cwd=libffi_dir, + quiet=context.quiet, + ) + write_library_config(prefix, "libffi", libffi_config, context.quiet) + + +@subdir("host_build_dir", clean_ok=True) +def make_mpdec(context, working_dir): + prefix = context.build_paths["prefix_dir"] + mpdec_config = load_config_toml()["mpdec"] + if not should_build_library(prefix, "mpdec", mpdec_config, context.quiet): + return + + url = mpdec_config["url"] + version = mpdec_config["version"] + shasum = mpdec_config["shasum"] + mpdec_dir = working_dir / f"mpdecimal-{version}" + shutil.rmtree(mpdec_dir, ignore_errors=True) + download_and_unpack( + working_dir, + url.format(version=version), + shasum, + ) + call( + [ + "emconfigure", + mpdec_dir / "configure", + "CFLAGS=-fPIC", + "--prefix", + prefix, + "--disable-shared", + ], + cwd=mpdec_dir, + quiet=context.quiet, + env=updated_env({}, context.emsdk_cache), + ) + call( + ["make", "install"], + cwd=mpdec_dir, + quiet=context.quiet, + ) + write_library_config(prefix, "mpdec", mpdec_config, context.quiet) + + +@subdir("host_dir", clean_ok=True) +def configure_emscripten_python(context, working_dir): + """Configure the emscripten/host build.""" + paths = context.build_paths + config_site = os.fsdecode(EMSCRIPTEN_DIR / "config.site-wasm32-emscripten") + + emscripten_build_dir = working_dir.relative_to(CHECKOUT) + + python_build_dir = paths["native_build_dir"] / "build" + lib_dirs = list(python_build_dir.glob("lib.*")) + assert len(lib_dirs) == 1, ( + f"Expected a single lib.* directory in {python_build_dir}" + ) + lib_dir = os.fsdecode(lib_dirs[0]) + pydebug = lib_dir.endswith("-pydebug") + python_version = lib_dir.removesuffix("-pydebug").rpartition("-")[-1] + sysconfig_data = ( + f"{emscripten_build_dir}/build/lib.emscripten-wasm32-{python_version}" + ) + if pydebug: + sysconfig_data += "-pydebug" + + host_runner = context.host_runner + if node_version := os.environ.get("PYTHON_NODE_VERSION", None): + res = subprocess.run( + [ + "bash", + "-c", + f"source ~/.nvm/nvm.sh && nvm which {node_version}", + ], + text=True, + capture_output=True, + ) + host_runner = res.stdout.strip() + pkg_config_path_dir = (paths["prefix_dir"] / "lib/pkgconfig/").resolve() + env_additions = { + "CONFIG_SITE": config_site, + "HOSTRUNNER": host_runner, + "EM_PKG_CONFIG_PATH": str(pkg_config_path_dir), + } + build_python = os.fsdecode(build_python_path(context)) + configure = [ + "emconfigure", + os.path.relpath(CHECKOUT / "configure", working_dir), + "CFLAGS=-DPY_CALL_TRAMPOLINE -sUSE_BZIP2", + "PKG_CONFIG=pkg-config", + f"--host={HOST_TRIPLE}", + f"--build={build_platform()}", + f"--with-build-python={build_python}", + "--without-pymalloc", + "--disable-shared", + "--disable-ipv6", + "--enable-big-digits=30", + "--enable-wasm-dynamic-linking", + f"--prefix={paths['prefix_dir']}", + ] + if pydebug: + configure.append("--with-pydebug") + if context.args: + configure.extend(context.args) + call( + configure, + env=updated_env(env_additions, context.emsdk_cache), + quiet=context.quiet, + ) + + shutil.copy( + EMSCRIPTEN_DIR / "node_entry.mjs", working_dir / "node_entry.mjs" + ) + + node_entry = working_dir / "node_entry.mjs" + exec_script = working_dir / "python.sh" + exec_script.write_text( + dedent( + f"""\ + #!/bin/sh + + # Macs come with FreeBSD coreutils which doesn't have the -s option + # so feature detect and work around it. + if which grealpath > /dev/null 2>&1; then + # It has brew installed gnu core utils, use that + REALPATH="grealpath -s" + elif which realpath > /dev/null 2>&1 && realpath --version > /dev/null 2>&1 && realpath --version | grep GNU > /dev/null 2>&1; then + # realpath points to GNU realpath so use it. + REALPATH="realpath -s" + else + # Shim for macs without GNU coreutils + abs_path () {{ + echo "$(cd "$(dirname "$1")" || exit; pwd)/$(basename "$1")" + }} + REALPATH=abs_path + fi + + # Before node 24, --experimental-wasm-jspi uses different API, + # After node 24 JSPI is on by default. + ARGS=$({host_runner} -e "$(cat <<"EOF" + const major_version = Number(process.version.split(".")[0].slice(1)); + if (major_version === 24) {{ + process.stdout.write("--experimental-wasm-jspi"); + }} + EOF + )") + + # We compute our own path, not following symlinks and pass it in so that + # node_entry.mjs can set sys.executable correctly. + # Intentionally allow word splitting on NODEFLAGS. + exec {host_runner} $NODEFLAGS $ARGS {node_entry} --this-program="$($REALPATH "$0")" "$@" + """ + ) + ) + exec_script.chmod(0o755) + print(f"๐Ÿƒโ€โ™€๏ธ Created {exec_script} ... ") + sys.stdout.flush() + + +@subdir("host_dir") +def make_emscripten_python(context, working_dir): + """Run `make` for the emscripten/host build.""" + call( + ["make", "--jobs", str(cpu_count()), "all"], + env=updated_env({}, context.emsdk_cache), + quiet=context.quiet, + ) + + exec_script = working_dir / "python.sh" + subprocess.check_call([exec_script, "--version"]) + + +def build_target(context): + """Build one or more targets.""" + steps = [] + if context.target in {"all"}: + steps.append(install_emscripten) + if context.target in {"build", "all"}: + steps.extend([ + configure_build_python, + make_build_python, + ]) + if context.target in {"host", "all"}: + steps.extend([ + make_emscripten_libffi, + make_mpdec, + configure_emscripten_python, + make_emscripten_python, + ]) + + for step in steps: + step(context) + + +def clean_contents(context): + """Delete all files created by this script.""" + if context.target in {"all", "build"}: + build_dir = context.build_paths["native_build_dir"] + if build_dir.exists(): + print(f"๐Ÿงน Deleting {build_dir} ...") + shutil.rmtree(build_dir) + + if context.target in {"all", "host"}: + host_triple_dir = context.build_paths["host_triple_dir"] + if host_triple_dir.exists(): + print(f"๐Ÿงน Deleting {host_triple_dir} ...") + shutil.rmtree(host_triple_dir) + + if LOCAL_SETUP.exists(): + with LOCAL_SETUP.open("rb") as file: + if file.read(len(LOCAL_SETUP_MARKER)) == LOCAL_SETUP_MARKER: + print(f"๐Ÿงน Deleting generated {LOCAL_SETUP} ...") + + +def main(): + default_host_runner = "node" + + parser = argparse.ArgumentParser() + subcommands = parser.add_subparsers(dest="subcommand") + install_emscripten_cmd = subcommands.add_parser( + "install-emscripten", + help="Install the appropriate version of Emscripten", + ) + build = subcommands.add_parser("build", help="Build everything") + build.add_argument( + "target", + nargs="?", + default="all", + choices=["all", "host", "build"], + help=( + "What should be built. 'build' for just the build platform, or " + "'host' for the host platform, or 'all' for both. Defaults to 'all'." + ), + ) + + configure_build = subcommands.add_parser( + "configure-build-python", help="Run `configure` for the build Python" + ) + make_mpdec_cmd = subcommands.add_parser( + "make-mpdec", + help="Clone mpdec repo, configure and build it for emscripten", + ) + make_libffi_cmd = subcommands.add_parser( + "make-libffi", + help="Clone libffi repo, configure and build it for emscripten", + ) + make_build = subcommands.add_parser( + "make-build-python", help="Run `make` for the build Python" + ) + configure_host = subcommands.add_parser( + "configure-host", + help="Run `configure` for the host/emscripten (pydebug builds are inferred from the build Python)", + ) + make_host = subcommands.add_parser( + "make-host", help="Run `make` for the host/emscripten" + ) + clean = subcommands.add_parser( + "clean", help="Delete files and directories created by this script" + ) + clean.add_argument( + "target", + nargs="?", + default="host", + choices=["all", "host", "build"], + help=( + "What should be cleaned. 'build' for just the build platform, or " + "'host' for the host platform, or 'all' for both. Defaults to 'host'." + ), + ) + + for subcommand in ( + install_emscripten_cmd, + build, + configure_build, + make_libffi_cmd, + make_mpdec_cmd, + make_build, + configure_host, + make_host, + clean, + ): + subcommand.add_argument( + "--quiet", + action="store_true", + default=False, + dest="quiet", + help="Redirect output from subprocesses to a log file", + ) + subcommand.add_argument( + "--cross-build-dir", + action="store", + default=None, + dest="cross_build_dir", + help="Path to the cross-build directory " + f"(default: {DEFAULT_CROSS_BUILD_DIR})", + ) + subcommand.add_argument( + "--emsdk-cache", + action="store", + default=None, + dest="emsdk_cache", + help="Path to emsdk cache directory. If provided, validates that " + "the required emscripten version is installed.", + ) + for subcommand in configure_build, configure_host: + subcommand.add_argument( + "--clean", + action="store_true", + default=False, + dest="clean", + help="Delete any relevant directories before building", + ) + for subcommand in build, configure_build, configure_host: + subcommand.add_argument( + "args", nargs="*", help="Extra arguments to pass to `configure`" + ) + for subcommand in build, configure_host: + subcommand.add_argument( + "--host-runner", + action="store", + default=default_host_runner, + dest="host_runner", + help="Command template for running the emscripten host" + f"`{default_host_runner}`)", + ) + + context = parser.parse_args() + + if context.emsdk_cache and context.subcommand != "install-emscripten": + validate_emsdk_version(context.emsdk_cache) + context.emsdk_cache = Path(context.emsdk_cache).absolute() + else: + print("Build will use EMSDK from current environment.") + + context.build_paths = get_build_paths( + context.cross_build_dir, context.emsdk_cache + ) + + dispatch = { + "install-emscripten": install_emscripten, + "make-libffi": make_emscripten_libffi, + "make-mpdec": make_mpdec, + "configure-build-python": configure_build_python, + "make-build-python": make_build_python, + "configure-host": configure_emscripten_python, + "make-host": make_emscripten_python, + "build": build_target, + "clean": clean_contents, + } + + if not context.subcommand: + # No command provided, display help and exit + print( + "Expected one of", + ", ".join(sorted(dispatch.keys())), + file=sys.stderr, + ) + parser.print_help(sys.stderr) + sys.exit(1) + dispatch[context.subcommand](context) + + +if __name__ == "__main__": + main() diff --git a/Tools/wasm/emscripten/browser_test/.gitignore b/Platforms/emscripten/browser_test/.gitignore similarity index 100% rename from Tools/wasm/emscripten/browser_test/.gitignore rename to Platforms/emscripten/browser_test/.gitignore diff --git a/Tools/wasm/emscripten/browser_test/index.spec.ts b/Platforms/emscripten/browser_test/index.spec.ts similarity index 100% rename from Tools/wasm/emscripten/browser_test/index.spec.ts rename to Platforms/emscripten/browser_test/index.spec.ts diff --git a/Tools/wasm/emscripten/browser_test/package-lock.json b/Platforms/emscripten/browser_test/package-lock.json similarity index 100% rename from Tools/wasm/emscripten/browser_test/package-lock.json rename to Platforms/emscripten/browser_test/package-lock.json diff --git a/Tools/wasm/emscripten/browser_test/package.json b/Platforms/emscripten/browser_test/package.json similarity index 100% rename from Tools/wasm/emscripten/browser_test/package.json rename to Platforms/emscripten/browser_test/package.json diff --git a/Tools/wasm/emscripten/browser_test/playwright.config.ts b/Platforms/emscripten/browser_test/playwright.config.ts similarity index 100% rename from Tools/wasm/emscripten/browser_test/playwright.config.ts rename to Platforms/emscripten/browser_test/playwright.config.ts diff --git a/Platforms/emscripten/browser_test/run_test.sh b/Platforms/emscripten/browser_test/run_test.sh new file mode 100755 index 00000000000000..9166e0d740585e --- /dev/null +++ b/Platforms/emscripten/browser_test/run_test.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail +cd "$(dirname "$0")" +rm -f test_log.txt +echo "Installing node packages" | tee test_log.txt +npm ci >> test_log.txt 2>&1 +echo "Installing playwright browsers" | tee test_log.txt +npx playwright install 2>> test_log.txt +echo "Running tests" | tee test_log.txt +CI=1 npx playwright test | tee test_log.txt diff --git a/Tools/wasm/emscripten/config.site-wasm32-emscripten b/Platforms/emscripten/config.site-wasm32-emscripten similarity index 97% rename from Tools/wasm/emscripten/config.site-wasm32-emscripten rename to Platforms/emscripten/config.site-wasm32-emscripten index 9f98e3f3c3bb1f..f69dbb8e779a42 100644 --- a/Tools/wasm/emscripten/config.site-wasm32-emscripten +++ b/Platforms/emscripten/config.site-wasm32-emscripten @@ -1,6 +1,6 @@ # config.site override for cross compiling to wasm32-emscripten platform # -# CONFIG_SITE=Tools/wasm/emscripten/config.site-wasm32-emscripten \ +# CONFIG_SITE=Platforms/emscripten/config.site-wasm32-emscripten \ # emconfigure ./configure --host=wasm32-unknown-emscripten --build=... # # Written by Christian Heimes diff --git a/Tools/wasm/emscripten/config.toml b/Platforms/emscripten/config.toml similarity index 100% rename from Tools/wasm/emscripten/config.toml rename to Platforms/emscripten/config.toml diff --git a/Tools/wasm/emscripten/make_libffi.sh b/Platforms/emscripten/make_libffi.sh similarity index 100% rename from Tools/wasm/emscripten/make_libffi.sh rename to Platforms/emscripten/make_libffi.sh diff --git a/Tools/wasm/emscripten/node_entry.mjs b/Platforms/emscripten/node_entry.mjs similarity index 100% rename from Tools/wasm/emscripten/node_entry.mjs rename to Platforms/emscripten/node_entry.mjs diff --git a/Tools/wasm/emscripten/prepare_external_wasm.py b/Platforms/emscripten/prepare_external_wasm.py similarity index 100% rename from Tools/wasm/emscripten/prepare_external_wasm.py rename to Platforms/emscripten/prepare_external_wasm.py diff --git a/Tools/wasm/emscripten/wasm_assets.py b/Platforms/emscripten/wasm_assets.py similarity index 99% rename from Tools/wasm/emscripten/wasm_assets.py rename to Platforms/emscripten/wasm_assets.py index 384790872353b2..8743e76e4449af 100755 --- a/Tools/wasm/emscripten/wasm_assets.py +++ b/Platforms/emscripten/wasm_assets.py @@ -17,7 +17,7 @@ import zipfile # source directory -SRCDIR = pathlib.Path(__file__).parents[3].absolute() +SRCDIR = pathlib.Path(__file__).parents[2].absolute() SRCDIR_LIB = SRCDIR / "Lib" diff --git a/Tools/wasm/emscripten/web_example/index.html b/Platforms/emscripten/web_example/index.html similarity index 100% rename from Tools/wasm/emscripten/web_example/index.html rename to Platforms/emscripten/web_example/index.html diff --git a/Tools/wasm/emscripten/web_example/python.worker.mjs b/Platforms/emscripten/web_example/python.worker.mjs similarity index 100% rename from Tools/wasm/emscripten/web_example/python.worker.mjs rename to Platforms/emscripten/web_example/python.worker.mjs diff --git a/Tools/wasm/emscripten/web_example/server.py b/Platforms/emscripten/web_example/server.py similarity index 100% rename from Tools/wasm/emscripten/web_example/server.py rename to Platforms/emscripten/web_example/server.py diff --git a/Tools/wasm/emscripten/web_example_pyrepl_jspi/index.html b/Platforms/emscripten/web_example_pyrepl_jspi/index.html similarity index 100% rename from Tools/wasm/emscripten/web_example_pyrepl_jspi/index.html rename to Platforms/emscripten/web_example_pyrepl_jspi/index.html diff --git a/Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs b/Platforms/emscripten/web_example_pyrepl_jspi/src.mjs similarity index 100% rename from Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs rename to Platforms/emscripten/web_example_pyrepl_jspi/src.mjs diff --git a/Tools/wasm/emscripten/__main__.py b/Tools/wasm/emscripten/__main__.py index b1a779777ae9fc..d04c4d77bc9cfe 100644 --- a/Tools/wasm/emscripten/__main__.py +++ b/Tools/wasm/emscripten/__main__.py @@ -1,729 +1,7 @@ -#!/usr/bin/env python3 - -import argparse -import contextlib -import functools -import hashlib -import json -import os -import shutil -import subprocess -import sys -import sysconfig -import tempfile -from pathlib import Path -from textwrap import dedent -from urllib.request import urlopen - -import tomllib - -try: - from os import process_cpu_count as cpu_count -except ImportError: - from os import cpu_count - - -EMSCRIPTEN_DIR = Path(__file__).parent -CHECKOUT = EMSCRIPTEN_DIR.parent.parent.parent -CONFIG_FILE = EMSCRIPTEN_DIR / "config.toml" - -DEFAULT_CROSS_BUILD_DIR = CHECKOUT / "cross-build" -HOST_TRIPLE = "wasm32-emscripten" - - -@functools.cache -def load_config_toml(): - with CONFIG_FILE.open("rb") as file: - return tomllib.load(file) - - -@functools.cache -def required_emscripten_version(): - return load_config_toml()["emscripten-version"] - - -@functools.cache -def emsdk_cache_root(emsdk_cache): - required_version = required_emscripten_version() - return Path(emsdk_cache).absolute() / required_version - - -@functools.cache -def emsdk_activate_path(emsdk_cache): - return emsdk_cache_root(emsdk_cache) / "emsdk/emsdk_env.sh" - - -def get_build_paths(cross_build_dir=None, emsdk_cache=None): - """Compute all build paths from the given cross-build directory.""" - if cross_build_dir is None: - cross_build_dir = DEFAULT_CROSS_BUILD_DIR - cross_build_dir = Path(cross_build_dir).absolute() - host_triple_dir = cross_build_dir / HOST_TRIPLE - prefix_dir = host_triple_dir / "prefix" - if emsdk_cache: - prefix_dir = emsdk_cache_root(emsdk_cache) / "prefix" - - return { - "cross_build_dir": cross_build_dir, - "native_build_dir": cross_build_dir / "build", - "host_triple_dir": host_triple_dir, - "host_build_dir": host_triple_dir / "build", - "host_dir": host_triple_dir / "build" / "python", - "prefix_dir": prefix_dir, - } - - -LOCAL_SETUP = CHECKOUT / "Modules" / "Setup.local" -LOCAL_SETUP_MARKER = b"# Generated by Tools/wasm/emscripten.py\n" - - -def validate_emsdk_version(emsdk_cache): - """Validate that the emsdk cache contains the required emscripten version.""" - required_version = required_emscripten_version() - emsdk_env = emsdk_activate_path(emsdk_cache) - if not emsdk_env.is_file(): - print( - f"Required emscripten version {required_version} not found in {emsdk_cache}", - file=sys.stderr, - ) - sys.exit(1) - print(f"โœ… Emscripten version {required_version} found in {emsdk_cache}") - - -def parse_env(text): - result = {} - for line in text.splitlines(): - key, val = line.split("=", 1) - result[key] = val - return result - - -@functools.cache -def get_emsdk_environ(emsdk_cache): - """Returns os.environ updated by sourcing emsdk_env.sh""" - if not emsdk_cache: - return os.environ - env_text = subprocess.check_output( - [ - "bash", - "-c", - f"EMSDK_QUIET=1 source {emsdk_activate_path(emsdk_cache)} && env", - ], - text=True, - ) - return parse_env(env_text) - - -def updated_env(updates, emsdk_cache): - """Create a new dict representing the environment to use. - - The changes made to the execution environment are printed out. - """ - env_defaults = {} - # https://reproducible-builds.org/docs/source-date-epoch/ - git_epoch_cmd = ["git", "log", "-1", "--pretty=%ct"] - try: - epoch = subprocess.check_output( - git_epoch_cmd, encoding="utf-8" - ).strip() - env_defaults["SOURCE_DATE_EPOCH"] = epoch - except subprocess.CalledProcessError: - pass # Might be building from a tarball. - # This layering lets SOURCE_DATE_EPOCH from os.environ takes precedence. - environment = env_defaults | get_emsdk_environ(emsdk_cache) | updates - env_diff = {} - for key, value in environment.items(): - if os.environ.get(key) != value: - env_diff[key] = value - - print("๐ŸŒŽ Environment changes:") - for key in sorted(env_diff.keys()): - print(f" {key}={env_diff[key]}") - - return environment - - -def subdir(path_key, *, clean_ok=False): - """Decorator to change to a working directory. - - path_key is a key into context.build_paths, used to resolve the working - directory at call time. - """ - - def decorator(func): - @functools.wraps(func) - def wrapper(context): - working_dir = context.build_paths[path_key] - try: - tput_output = subprocess.check_output( - ["tput", "cols"], encoding="utf-8" - ) - terminal_width = int(tput_output.strip()) - except subprocess.CalledProcessError: - terminal_width = 80 - print("โŽฏ" * terminal_width) - print("๐Ÿ“", working_dir) - if ( - clean_ok - and getattr(context, "clean", False) - and working_dir.exists() - ): - print("๐Ÿšฎ Deleting directory (--clean)...") - shutil.rmtree(working_dir) - - working_dir.mkdir(parents=True, exist_ok=True) - - with contextlib.chdir(working_dir): - return func(context, working_dir) - - return wrapper - - return decorator - - -def call(command, *, quiet, **kwargs): - """Execute a command. - - If 'quiet' is true, then redirect stdout and stderr to a temporary file. - """ - print("โฏ", " ".join(map(str, command))) - if not quiet: - stdout = None - stderr = None - else: - stdout = tempfile.NamedTemporaryFile( - "w", - encoding="utf-8", - delete=False, - prefix="cpython-emscripten-", - suffix=".log", - ) - stderr = subprocess.STDOUT - print(f"๐Ÿ“ Logging output to {stdout.name} (--quiet)...") - - subprocess.check_call(command, **kwargs, stdout=stdout, stderr=stderr) - - -def build_platform(): - """The name of the build/host platform.""" - # Can also be found via `config.guess`.` - return sysconfig.get_config_var("BUILD_GNU_TYPE") - - -def build_python_path(context): - """The path to the build Python binary.""" - native_build_dir = context.build_paths["native_build_dir"] - binary = native_build_dir / "python" - if not binary.is_file(): - binary = binary.with_suffix(".exe") - if not binary.is_file(): - raise FileNotFoundError( - f"Unable to find `python(.exe)` in {native_build_dir}" - ) - - return binary - - -def install_emscripten(context): - emsdk_cache = context.emsdk_cache - if emsdk_cache is None: - print("install-emscripten requires --emsdk-cache", file=sys.stderr) - sys.exit(1) - version = required_emscripten_version() - emsdk_target = emsdk_cache_root(emsdk_cache) / "emsdk" - if emsdk_target.exists(): - if not context.quiet: - print(f"Emscripten version {version} already installed") - return - if not context.quiet: - print(f"Installing emscripten version {version}") - emsdk_target.mkdir(parents=True) - call( - [ - "git", - "clone", - "https://github.com/emscripten-core/emsdk.git", - emsdk_target, - ], - quiet=context.quiet, - ) - call([emsdk_target / "emsdk", "install", version], quiet=context.quiet) - call([emsdk_target / "emsdk", "activate", version], quiet=context.quiet) - if not context.quiet: - print(f"Installed emscripten version {version}") - - -@subdir("native_build_dir", clean_ok=True) -def configure_build_python(context, working_dir): - """Configure the build/host Python.""" - if LOCAL_SETUP.exists(): - print(f"๐Ÿ‘ {LOCAL_SETUP} exists ...") - else: - print(f"๐Ÿ“ Touching {LOCAL_SETUP} ...") - LOCAL_SETUP.write_bytes(LOCAL_SETUP_MARKER) - - configure = [os.path.relpath(CHECKOUT / "configure", working_dir)] - if context.args: - configure.extend(context.args) - - call(configure, quiet=context.quiet) - - -@subdir("native_build_dir") -def make_build_python(context, working_dir): - """Make/build the build Python.""" - call(["make", "--jobs", str(cpu_count()), "all"], quiet=context.quiet) - - binary = build_python_path(context) - cmd = [ - binary, - "-c", - "import sys; " - "print(f'{sys.version_info.major}.{sys.version_info.minor}')", - ] - version = subprocess.check_output(cmd, encoding="utf-8").strip() - - print(f"๐ŸŽ‰ {binary} {version}") - - -def check_shasum(file: str, expected_shasum: str): - with open(file, "rb") as f: - digest = hashlib.file_digest(f, "sha256") - if digest.hexdigest() != expected_shasum: - raise RuntimeError(f"Unexpected shasum for {file}") - - -def download_and_unpack(working_dir: Path, url: str, expected_shasum: str): - with tempfile.NamedTemporaryFile( - suffix=".tar.gz", delete_on_close=False - ) as tmp_file: - with urlopen(url) as response: - shutil.copyfileobj(response, tmp_file) - tmp_file.close() - check_shasum(tmp_file.name, expected_shasum) - shutil.unpack_archive(tmp_file.name, working_dir) - - -def should_build_library(prefix, name, config, quiet): - cached_config = prefix / (name + ".json") - if not cached_config.exists(): - if not quiet: - print( - f"No cached build of {name} version {config['version']} found, building" - ) - return True - - try: - with cached_config.open("rb") as f: - cached_config = json.load(f) - except json.JSONDecodeError: - if not quiet: - print(f"Cached data for {name} invalid, rebuilding") - return True - if config == cached_config: - if not quiet: - print( - f"Found cached build of {name} version {config['version']}, not rebuilding" - ) - return False - - if not quiet: - print( - f"Found cached build of {name} version {config['version']} but it's out of date, rebuilding" - ) - return True - - -def write_library_config(prefix, name, config, quiet): - cached_config = prefix / (name + ".json") - with cached_config.open("w") as f: - json.dump(config, f) - if not quiet: - print(f"Succeded building {name}, wrote config to {cached_config}") - - -@subdir("host_build_dir", clean_ok=True) -def make_emscripten_libffi(context, working_dir): - prefix = context.build_paths["prefix_dir"] - libffi_config = load_config_toml()["libffi"] - if not should_build_library( - prefix, "libffi", libffi_config, context.quiet - ): - return - url = libffi_config["url"] - version = libffi_config["version"] - shasum = libffi_config["shasum"] - libffi_dir = working_dir / f"libffi-{version}" - shutil.rmtree(libffi_dir, ignore_errors=True) - download_and_unpack( - working_dir, - url.format(version=version), - shasum, - ) - call( - [EMSCRIPTEN_DIR / "make_libffi.sh"], - env=updated_env({"PREFIX": prefix}, context.emsdk_cache), - cwd=libffi_dir, - quiet=context.quiet, - ) - write_library_config(prefix, "libffi", libffi_config, context.quiet) - - -@subdir("host_build_dir", clean_ok=True) -def make_mpdec(context, working_dir): - prefix = context.build_paths["prefix_dir"] - mpdec_config = load_config_toml()["mpdec"] - if not should_build_library(prefix, "mpdec", mpdec_config, context.quiet): - return - - url = mpdec_config["url"] - version = mpdec_config["version"] - shasum = mpdec_config["shasum"] - mpdec_dir = working_dir / f"mpdecimal-{version}" - shutil.rmtree(mpdec_dir, ignore_errors=True) - download_and_unpack( - working_dir, - url.format(version=version), - shasum, - ) - call( - [ - "emconfigure", - mpdec_dir / "configure", - "CFLAGS=-fPIC", - "--prefix", - prefix, - "--disable-shared", - ], - cwd=mpdec_dir, - quiet=context.quiet, - env=updated_env({}, context.emsdk_cache), - ) - call( - ["make", "install"], - cwd=mpdec_dir, - quiet=context.quiet, - ) - write_library_config(prefix, "mpdec", mpdec_config, context.quiet) - - -@subdir("host_dir", clean_ok=True) -def configure_emscripten_python(context, working_dir): - """Configure the emscripten/host build.""" - paths = context.build_paths - config_site = os.fsdecode(EMSCRIPTEN_DIR / "config.site-wasm32-emscripten") - - emscripten_build_dir = working_dir.relative_to(CHECKOUT) - - python_build_dir = paths["native_build_dir"] / "build" - lib_dirs = list(python_build_dir.glob("lib.*")) - assert len(lib_dirs) == 1, ( - f"Expected a single lib.* directory in {python_build_dir}" - ) - lib_dir = os.fsdecode(lib_dirs[0]) - pydebug = lib_dir.endswith("-pydebug") - python_version = lib_dir.removesuffix("-pydebug").rpartition("-")[-1] - sysconfig_data = ( - f"{emscripten_build_dir}/build/lib.emscripten-wasm32-{python_version}" - ) - if pydebug: - sysconfig_data += "-pydebug" - - host_runner = context.host_runner - if node_version := os.environ.get("PYTHON_NODE_VERSION", None): - res = subprocess.run( - [ - "bash", - "-c", - f"source ~/.nvm/nvm.sh && nvm which {node_version}", - ], - text=True, - capture_output=True, - ) - host_runner = res.stdout.strip() - pkg_config_path_dir = (paths["prefix_dir"] / "lib/pkgconfig/").resolve() - env_additions = { - "CONFIG_SITE": config_site, - "HOSTRUNNER": host_runner, - "EM_PKG_CONFIG_PATH": str(pkg_config_path_dir), - } - build_python = os.fsdecode(build_python_path(context)) - configure = [ - "emconfigure", - os.path.relpath(CHECKOUT / "configure", working_dir), - "CFLAGS=-DPY_CALL_TRAMPOLINE -sUSE_BZIP2", - "PKG_CONFIG=pkg-config", - f"--host={HOST_TRIPLE}", - f"--build={build_platform()}", - f"--with-build-python={build_python}", - "--without-pymalloc", - "--disable-shared", - "--disable-ipv6", - "--enable-big-digits=30", - "--enable-wasm-dynamic-linking", - f"--prefix={paths['prefix_dir']}", - ] - if pydebug: - configure.append("--with-pydebug") - if context.args: - configure.extend(context.args) - call( - configure, - env=updated_env(env_additions, context.emsdk_cache), - quiet=context.quiet, - ) - - shutil.copy( - EMSCRIPTEN_DIR / "node_entry.mjs", working_dir / "node_entry.mjs" - ) - - node_entry = working_dir / "node_entry.mjs" - exec_script = working_dir / "python.sh" - exec_script.write_text( - dedent( - f"""\ - #!/bin/sh - - # Macs come with FreeBSD coreutils which doesn't have the -s option - # so feature detect and work around it. - if which grealpath > /dev/null 2>&1; then - # It has brew installed gnu core utils, use that - REALPATH="grealpath -s" - elif which realpath > /dev/null 2>&1 && realpath --version > /dev/null 2>&1 && realpath --version | grep GNU > /dev/null 2>&1; then - # realpath points to GNU realpath so use it. - REALPATH="realpath -s" - else - # Shim for macs without GNU coreutils - abs_path () {{ - echo "$(cd "$(dirname "$1")" || exit; pwd)/$(basename "$1")" - }} - REALPATH=abs_path - fi - - # Before node 24, --experimental-wasm-jspi uses different API, - # After node 24 JSPI is on by default. - ARGS=$({host_runner} -e "$(cat <<"EOF" - const major_version = Number(process.version.split(".")[0].slice(1)); - if (major_version === 24) {{ - process.stdout.write("--experimental-wasm-jspi"); - }} - EOF - )") - - # We compute our own path, not following symlinks and pass it in so that - # node_entry.mjs can set sys.executable correctly. - # Intentionally allow word splitting on NODEFLAGS. - exec {host_runner} $NODEFLAGS $ARGS {node_entry} --this-program="$($REALPATH "$0")" "$@" - """ - ) - ) - exec_script.chmod(0o755) - print(f"๐Ÿƒโ€โ™€๏ธ Created {exec_script} ... ") - sys.stdout.flush() - - -@subdir("host_dir") -def make_emscripten_python(context, working_dir): - """Run `make` for the emscripten/host build.""" - call( - ["make", "--jobs", str(cpu_count()), "all"], - env=updated_env({}, context.emsdk_cache), - quiet=context.quiet, - ) - - exec_script = working_dir / "python.sh" - subprocess.check_call([exec_script, "--version"]) - - -def build_target(context): - """Build one or more targets.""" - steps = [] - if context.target in {"all"}: - steps.append(install_emscripten) - if context.target in {"build", "all"}: - steps.extend([ - configure_build_python, - make_build_python, - ]) - if context.target in {"host", "all"}: - steps.extend([ - make_emscripten_libffi, - make_mpdec, - configure_emscripten_python, - make_emscripten_python, - ]) - - for step in steps: - step(context) - - -def clean_contents(context): - """Delete all files created by this script.""" - if context.target in {"all", "build"}: - build_dir = context.build_paths["native_build_dir"] - if build_dir.exists(): - print(f"๐Ÿงน Deleting {build_dir} ...") - shutil.rmtree(build_dir) - - if context.target in {"all", "host"}: - host_triple_dir = context.build_paths["host_triple_dir"] - if host_triple_dir.exists(): - print(f"๐Ÿงน Deleting {host_triple_dir} ...") - shutil.rmtree(host_triple_dir) - - if LOCAL_SETUP.exists(): - with LOCAL_SETUP.open("rb") as file: - if file.read(len(LOCAL_SETUP_MARKER)) == LOCAL_SETUP_MARKER: - print(f"๐Ÿงน Deleting generated {LOCAL_SETUP} ...") - - -def main(): - default_host_runner = "node" - - parser = argparse.ArgumentParser() - subcommands = parser.add_subparsers(dest="subcommand") - install_emscripten_cmd = subcommands.add_parser( - "install-emscripten", - help="Install the appropriate version of Emscripten", - ) - build = subcommands.add_parser("build", help="Build everything") - build.add_argument( - "target", - nargs="?", - default="all", - choices=["all", "host", "build"], - help=( - "What should be built. 'build' for just the build platform, or " - "'host' for the host platform, or 'all' for both. Defaults to 'all'." - ), - ) - - configure_build = subcommands.add_parser( - "configure-build-python", help="Run `configure` for the build Python" - ) - make_mpdec_cmd = subcommands.add_parser( - "make-mpdec", - help="Clone mpdec repo, configure and build it for emscripten", - ) - make_libffi_cmd = subcommands.add_parser( - "make-libffi", - help="Clone libffi repo, configure and build it for emscripten", - ) - make_build = subcommands.add_parser( - "make-build-python", help="Run `make` for the build Python" - ) - configure_host = subcommands.add_parser( - "configure-host", - help="Run `configure` for the host/emscripten (pydebug builds are inferred from the build Python)", - ) - make_host = subcommands.add_parser( - "make-host", help="Run `make` for the host/emscripten" - ) - clean = subcommands.add_parser( - "clean", help="Delete files and directories created by this script" - ) - clean.add_argument( - "target", - nargs="?", - default="host", - choices=["all", "host", "build"], - help=( - "What should be cleaned. 'build' for just the build platform, or " - "'host' for the host platform, or 'all' for both. Defaults to 'host'." - ), - ) - - for subcommand in ( - install_emscripten_cmd, - build, - configure_build, - make_libffi_cmd, - make_mpdec_cmd, - make_build, - configure_host, - make_host, - clean, - ): - subcommand.add_argument( - "--quiet", - action="store_true", - default=False, - dest="quiet", - help="Redirect output from subprocesses to a log file", - ) - subcommand.add_argument( - "--cross-build-dir", - action="store", - default=None, - dest="cross_build_dir", - help="Path to the cross-build directory " - f"(default: {DEFAULT_CROSS_BUILD_DIR})", - ) - subcommand.add_argument( - "--emsdk-cache", - action="store", - default=None, - dest="emsdk_cache", - help="Path to emsdk cache directory. If provided, validates that " - "the required emscripten version is installed.", - ) - for subcommand in configure_build, configure_host: - subcommand.add_argument( - "--clean", - action="store_true", - default=False, - dest="clean", - help="Delete any relevant directories before building", - ) - for subcommand in build, configure_build, configure_host: - subcommand.add_argument( - "args", nargs="*", help="Extra arguments to pass to `configure`" - ) - for subcommand in build, configure_host: - subcommand.add_argument( - "--host-runner", - action="store", - default=default_host_runner, - dest="host_runner", - help="Command template for running the emscripten host" - f"`{default_host_runner}`)", - ) - - context = parser.parse_args() - - if context.emsdk_cache and context.subcommand != "install-emscripten": - validate_emsdk_version(context.emsdk_cache) - context.emsdk_cache = Path(context.emsdk_cache).absolute() - else: - print("Build will use EMSDK from current environment.") - - context.build_paths = get_build_paths( - context.cross_build_dir, context.emsdk_cache - ) - - dispatch = { - "install-emscripten": install_emscripten, - "make-libffi": make_emscripten_libffi, - "make-mpdec": make_mpdec, - "configure-build-python": configure_build_python, - "make-build-python": make_build_python, - "configure-host": configure_emscripten_python, - "make-host": make_emscripten_python, - "build": build_target, - "clean": clean_contents, - } - - if not context.subcommand: - # No command provided, display help and exit - print( - "Expected one of", - ", ".join(sorted(dispatch.keys())), - file=sys.stderr, - ) - parser.print_help(sys.stderr) - sys.exit(1) - dispatch[context.subcommand](context) - - if __name__ == "__main__": - main() + import pathlib + import runpy + + checkout = pathlib.Path(__file__).parents[3] + emscripten_dir = (checkout / "Platforms/emscripten").absolute() + runpy.run_path(str(emscripten_dir), run_name="__main__") diff --git a/Tools/wasm/emscripten/browser_test/run_test.sh b/Tools/wasm/emscripten/browser_test/run_test.sh index 9166e0d740585e..ed8cae7bf23b29 100755 --- a/Tools/wasm/emscripten/browser_test/run_test.sh +++ b/Tools/wasm/emscripten/browser_test/run_test.sh @@ -1,10 +1,3 @@ #!/bin/bash -set -euo pipefail -cd "$(dirname "$0")" -rm -f test_log.txt -echo "Installing node packages" | tee test_log.txt -npm ci >> test_log.txt 2>&1 -echo "Installing playwright browsers" | tee test_log.txt -npx playwright install 2>> test_log.txt -echo "Running tests" | tee test_log.txt -CI=1 npx playwright test | tee test_log.txt +# Redirect to new location +exec "$(dirname "$0")/../../../../Platforms/emscripten/browser_test/run_test.sh" "$@"