https://github.com/python/cpython/commit/1b118353bb0a9d816de6ef673f3b11775de5bec5
commit: 1b118353bb0a9d816de6ef673f3b11775de5bec5
branch: main
author: Hood Chatham <[email protected]>
committer: freakboy3742 <[email protected]>
date: 2026-03-17T09:39:45+08:00
summary:

gh-145176 Move Emscripten files into Platforms/emscripten (#145806)

Moves Emscripten build files into Platforms/emscripten.

files:
A Platforms/emscripten/README.md
A Platforms/emscripten/__main__.py
A Platforms/emscripten/browser_test/.gitignore
A Platforms/emscripten/browser_test/index.spec.ts
A Platforms/emscripten/browser_test/package-lock.json
A Platforms/emscripten/browser_test/package.json
A Platforms/emscripten/browser_test/playwright.config.ts
A Platforms/emscripten/browser_test/run_test.sh
A Platforms/emscripten/config.site-wasm32-emscripten
A Platforms/emscripten/config.toml
A Platforms/emscripten/make_libffi.sh
A Platforms/emscripten/node_entry.mjs
A Platforms/emscripten/prepare_external_wasm.py
A Platforms/emscripten/wasm_assets.py
A Platforms/emscripten/web_example/index.html
A Platforms/emscripten/web_example/python.worker.mjs
A Platforms/emscripten/web_example/server.py
A Platforms/emscripten/web_example_pyrepl_jspi/index.html
A Platforms/emscripten/web_example_pyrepl_jspi/src.mjs
D Tools/wasm/README.md
D Tools/wasm/emscripten/browser_test/.gitignore
D Tools/wasm/emscripten/browser_test/index.spec.ts
D Tools/wasm/emscripten/browser_test/package-lock.json
D Tools/wasm/emscripten/browser_test/package.json
D Tools/wasm/emscripten/browser_test/playwright.config.ts
D Tools/wasm/emscripten/config.site-wasm32-emscripten
D Tools/wasm/emscripten/config.toml
D Tools/wasm/emscripten/make_libffi.sh
D Tools/wasm/emscripten/node_entry.mjs
D Tools/wasm/emscripten/prepare_external_wasm.py
D Tools/wasm/emscripten/wasm_assets.py
D Tools/wasm/emscripten/web_example/index.html
D Tools/wasm/emscripten/web_example/python.worker.mjs
D Tools/wasm/emscripten/web_example/server.py
D Tools/wasm/emscripten/web_example_pyrepl_jspi/index.html
D Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs
M Makefile.pre.in
M Tools/wasm/emscripten/__main__.py
M Tools/wasm/emscripten/browser_test/run_test.sh

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..7b5f6d2ab1bdd9
--- /dev/null
+++ b/Platforms/emscripten/__main__.py
@@ -0,0 +1,734 @@
+#!/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"
+
+
[email protected]
+def load_config_toml():
+    with CONFIG_FILE.open("rb") as file:
+        return tomllib.load(file)
+
+
[email protected]
+def required_emscripten_version():
+    return load_config_toml()["emscripten-version"]
+
+
[email protected]
+def emsdk_cache_root(emsdk_cache):
+    required_version = required_emscripten_version()
+    return Path(emsdk_cache).absolute() / required_version
+
+
[email protected]
+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."""
+    if emsdk_cache is None:
+        return
+    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
+
+
[email protected]
+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):
+    validate_emsdk_version(context.emsdk_cache)
+    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):
+    validate_emsdk_version(context.emsdk_cache)
+    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."""
+    validate_emsdk_version(context.emsdk_cache)
+    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."""
+    validate_emsdk_version(context.emsdk_cache)
+    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 {"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()
+    context.emsdk_cache = getattr(context, "emsdk_cache", None)
+    context.cross_build_dir = getattr(context, "cross_build_dir", None)
+
+    if 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 77%
rename from Tools/wasm/emscripten/browser_test/playwright.config.ts
rename to Platforms/emscripten/browser_test/playwright.config.ts
index 81d53ce11cb050..0b38beb12826a9 100644
--- a/Tools/wasm/emscripten/browser_test/playwright.config.ts
+++ b/Platforms/emscripten/browser_test/playwright.config.ts
@@ -16,7 +16,7 @@ export default defineConfig({
     },
   ],
   webServer: {
-    command: 'npx http-server 
../../../../cross-build/wasm32-emscripten/build/python/web_example_pyrepl_jspi/ 
-p 8787',
+    command: 'npx http-server 
../../../cross-build/wasm32-emscripten/build/python/web_example_pyrepl_jspi/ -p 
8787',
     url: 'http://localhost:8787',
   },
 });
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 <[email protected]>
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..29890cc1a2f365 100644
--- a/Tools/wasm/emscripten/__main__.py
+++ b/Tools/wasm/emscripten/__main__.py
@@ -1,729 +1,14 @@
-#!/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"
-
-
[email protected]
-def load_config_toml():
-    with CONFIG_FILE.open("rb") as file:
-        return tomllib.load(file)
-
-
[email protected]
-def required_emscripten_version():
-    return load_config_toml()["emscripten-version"]
-
-
[email protected]
-def emsdk_cache_root(emsdk_cache):
-    required_version = required_emscripten_version()
-    return Path(emsdk_cache).absolute() / required_version
-
-
[email protected]
-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
-
-
[email protected]
-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.")
+if __name__ == "__main__":
+    import pathlib
+    import runpy
+    import sys
 
-    context.build_paths = get_build_paths(
-        context.cross_build_dir, context.emsdk_cache
+    print(
+        "โš ๏ธ WARNING: This script is deprecated and slated for removal in 
Python 3.20; "
+        "execute the `Platforms/emscripten/` directory instead (i.e. `python 
Platforms/emscripten`)\n",
+        file=sys.stderr,
     )
 
-    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()
+    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" "$@"

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to