mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 07:31:38 +00:00 
			
		
		
		
	
		
			
	
	
		
			908 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			908 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
								 | 
							
								#!/usr/bin/env python3
							 | 
						||
| 
								 | 
							
								"""Build script for Python on WebAssembly platforms.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  $ ./Tools/wasm/wasm_builder.py emscripten-browser build repl
							 | 
						||
| 
								 | 
							
								  $ ./Tools/wasm/wasm_builder.py emscripten-node-dl build test
							 | 
						||
| 
								 | 
							
								  $ ./Tools/wasm/wasm_builder.py wasi build test
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Primary build targets are "emscripten-node-dl" (NodeJS, dynamic linking),
							 | 
						||
| 
								 | 
							
								"emscripten-browser", and "wasi".
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Emscripten builds require a recent Emscripten SDK. The tools looks for an
							 | 
						||
| 
								 | 
							
								activated EMSDK environment (". /path/to/emsdk_env.sh"). System packages
							 | 
						||
| 
								 | 
							
								(Debian, Homebrew) are not supported.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								WASI builds require WASI SDK and wasmtime. The tool looks for 'WASI_SDK_PATH'
							 | 
						||
| 
								 | 
							
								and falls back to /opt/wasi-sdk.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The 'build' Python interpreter must be rebuilt every time Python's byte code
							 | 
						||
| 
								 | 
							
								changes.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  ./Tools/wasm/wasm_builder.py --clean build build
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								"""
							 | 
						||
| 
								 | 
							
								import argparse
							 | 
						||
| 
								 | 
							
								import enum
							 | 
						||
| 
								 | 
							
								import dataclasses
							 | 
						||
| 
								 | 
							
								import logging
							 | 
						||
| 
								 | 
							
								import os
							 | 
						||
| 
								 | 
							
								import pathlib
							 | 
						||
| 
								 | 
							
								import re
							 | 
						||
| 
								 | 
							
								import shlex
							 | 
						||
| 
								 | 
							
								import shutil
							 | 
						||
| 
								 | 
							
								import socket
							 | 
						||
| 
								 | 
							
								import subprocess
							 | 
						||
| 
								 | 
							
								import sys
							 | 
						||
| 
								 | 
							
								import sysconfig
							 | 
						||
| 
								 | 
							
								import tempfile
							 | 
						||
| 
								 | 
							
								import time
							 | 
						||
| 
								 | 
							
								import warnings
							 | 
						||
| 
								 | 
							
								import webbrowser
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# for Python 3.8
							 | 
						||
| 
								 | 
							
								from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								logger = logging.getLogger("wasm_build")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								SRCDIR = pathlib.Path(__file__).parent.parent.parent.absolute()
							 | 
						||
| 
								 | 
							
								WASMTOOLS = SRCDIR / "Tools" / "wasm"
							 | 
						||
| 
								 | 
							
								BUILDDIR = SRCDIR / "builddir"
							 | 
						||
| 
								 | 
							
								CONFIGURE = SRCDIR / "configure"
							 | 
						||
| 
								 | 
							
								SETUP_LOCAL = SRCDIR / "Modules" / "Setup.local"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								HAS_CCACHE = shutil.which("ccache") is not None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# path to WASI-SDK root
							 | 
						||
| 
								 | 
							
								WASI_SDK_PATH = pathlib.Path(os.environ.get("WASI_SDK_PATH", "/opt/wasi-sdk"))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# path to Emscripten SDK config file.
							 | 
						||
| 
								 | 
							
								# auto-detect's EMSDK in /opt/emsdk without ". emsdk_env.sh".
							 | 
						||
| 
								 | 
							
								EM_CONFIG = pathlib.Path(os.environ.setdefault("EM_CONFIG", "/opt/emsdk/.emscripten"))
							 | 
						||
| 
								 | 
							
								EMSDK_MIN_VERSION = (3, 1, 19)
							 | 
						||
| 
								 | 
							
								EMSDK_BROKEN_VERSION = {
							 | 
						||
| 
								 | 
							
								    (3, 1, 14): "https://github.com/emscripten-core/emscripten/issues/17338",
							 | 
						||
| 
								 | 
							
								    (3, 1, 16): "https://github.com/emscripten-core/emscripten/issues/17393",
							 | 
						||
| 
								 | 
							
								    (3, 1, 20): "https://github.com/emscripten-core/emscripten/issues/17720",
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								_MISSING = pathlib.PurePath("MISSING")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								WASM_WEBSERVER = WASMTOOLS / "wasm_webserver.py"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								CLEAN_SRCDIR = f"""
							 | 
						||
| 
								 | 
							
								Builds require a clean source directory. Please use a clean checkout or
							 | 
						||
| 
								 | 
							
								run "make clean -C '{SRCDIR}'".
							 | 
						||
| 
								 | 
							
								"""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								INSTALL_NATIVE = f"""
							 | 
						||
| 
								 | 
							
								Builds require a C compiler (gcc, clang), make, pkg-config, and development
							 | 
						||
| 
								 | 
							
								headers for dependencies like zlib.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Debian/Ubuntu: sudo apt install build-essential git curl pkg-config zlib1g-dev
							 | 
						||
| 
								 | 
							
								Fedora/CentOS: sudo dnf install gcc make git-core curl pkgconfig zlib-devel
							 | 
						||
| 
								 | 
							
								"""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								INSTALL_EMSDK = """
							 | 
						||
| 
								 | 
							
								wasm32-emscripten builds need Emscripten SDK. Please follow instructions at
							 | 
						||
| 
								 | 
							
								https://emscripten.org/docs/getting_started/downloads.html how to install
							 | 
						||
| 
								 | 
							
								Emscripten and how to activate the SDK with "emsdk_env.sh".
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    git clone https://github.com/emscripten-core/emsdk.git /path/to/emsdk
							 | 
						||
| 
								 | 
							
								    cd /path/to/emsdk
							 | 
						||
| 
								 | 
							
								    ./emsdk install latest
							 | 
						||
| 
								 | 
							
								    ./emsdk activate latest
							 | 
						||
| 
								 | 
							
								    source /path/to/emsdk_env.sh
							 | 
						||
| 
								 | 
							
								"""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								INSTALL_WASI_SDK = """
							 | 
						||
| 
								 | 
							
								wasm32-wasi builds need WASI SDK. Please fetch the latest SDK from
							 | 
						||
| 
								 | 
							
								https://github.com/WebAssembly/wasi-sdk/releases and install it to
							 | 
						||
| 
								 | 
							
								"/opt/wasi-sdk". Alternatively you can install the SDK in a different location
							 | 
						||
| 
								 | 
							
								and point the environment variable WASI_SDK_PATH to the root directory
							 | 
						||
| 
								 | 
							
								of the SDK. The SDK is available for Linux x86_64, macOS x86_64, and MinGW.
							 | 
						||
| 
								 | 
							
								"""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								INSTALL_WASMTIME = """
							 | 
						||
| 
								 | 
							
								wasm32-wasi tests require wasmtime on PATH. Please follow instructions at
							 | 
						||
| 
								 | 
							
								https://wasmtime.dev/ to install wasmtime.
							 | 
						||
| 
								 | 
							
								"""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def parse_emconfig(
							 | 
						||
| 
								 | 
							
								    emconfig: pathlib.Path = EM_CONFIG,
							 | 
						||
| 
								 | 
							
								) -> Tuple[pathlib.PurePath, pathlib.PurePath]:
							 | 
						||
| 
								 | 
							
								    """Parse EM_CONFIG file and lookup EMSCRIPTEN_ROOT and NODE_JS.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    The ".emscripten" config file is a Python snippet that uses "EM_CONFIG"
							 | 
						||
| 
								 | 
							
								    environment variable. EMSCRIPTEN_ROOT is the "upstream/emscripten"
							 | 
						||
| 
								 | 
							
								    subdirectory with tools like "emconfigure".
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    if not emconfig.exists():
							 | 
						||
| 
								 | 
							
								        return _MISSING, _MISSING
							 | 
						||
| 
								 | 
							
								    with open(emconfig, encoding="utf-8") as f:
							 | 
						||
| 
								 | 
							
								        code = f.read()
							 | 
						||
| 
								 | 
							
								    # EM_CONFIG file is a Python snippet
							 | 
						||
| 
								 | 
							
								    local: Dict[str, Any] = {}
							 | 
						||
| 
								 | 
							
								    exec(code, globals(), local)
							 | 
						||
| 
								 | 
							
								    emscripten_root = pathlib.Path(local["EMSCRIPTEN_ROOT"])
							 | 
						||
| 
								 | 
							
								    node_js = pathlib.Path(local["NODE_JS"])
							 | 
						||
| 
								 | 
							
								    return emscripten_root, node_js
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								EMSCRIPTEN_ROOT, NODE_JS = parse_emconfig()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def read_python_version(configure: pathlib.Path = CONFIGURE) -> str:
							 | 
						||
| 
								 | 
							
								    """Read PACKAGE_VERSION from configure script
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    configure and configure.ac are the canonical source for major and
							 | 
						||
| 
								 | 
							
								    minor version number.
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    version_re = re.compile("^PACKAGE_VERSION='(\d\.\d+)'")
							 | 
						||
| 
								 | 
							
								    with configure.open(encoding="utf-8") as f:
							 | 
						||
| 
								 | 
							
								        for line in f:
							 | 
						||
| 
								 | 
							
								            mo = version_re.match(line)
							 | 
						||
| 
								 | 
							
								            if mo:
							 | 
						||
| 
								 | 
							
								                return mo.group(1)
							 | 
						||
| 
								 | 
							
								    raise ValueError(f"PACKAGE_VERSION not found in {configure}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								PYTHON_VERSION = read_python_version()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class ConditionError(ValueError):
							 | 
						||
| 
								 | 
							
								    def __init__(self, info: str, text: str):
							 | 
						||
| 
								 | 
							
								        self.info = info
							 | 
						||
| 
								 | 
							
								        self.text = text
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def __str__(self):
							 | 
						||
| 
								 | 
							
								        return f"{type(self).__name__}: '{self.info}'\n{self.text}"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class MissingDependency(ConditionError):
							 | 
						||
| 
								 | 
							
								    pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class DirtySourceDirectory(ConditionError):
							 | 
						||
| 
								 | 
							
								    pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								@dataclasses.dataclass
							 | 
						||
| 
								 | 
							
								class Platform:
							 | 
						||
| 
								 | 
							
								    """Platform-specific settings
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    - CONFIG_SITE override
							 | 
						||
| 
								 | 
							
								    - configure wrapper (e.g. emconfigure)
							 | 
						||
| 
								 | 
							
								    - make wrapper (e.g. emmake)
							 | 
						||
| 
								 | 
							
								    - additional environment variables
							 | 
						||
| 
								 | 
							
								    - check function to verify SDK
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    name: str
							 | 
						||
| 
								 | 
							
								    pythonexe: str
							 | 
						||
| 
								 | 
							
								    config_site: Optional[pathlib.PurePath]
							 | 
						||
| 
								 | 
							
								    configure_wrapper: Optional[pathlib.PurePath]
							 | 
						||
| 
								 | 
							
								    make_wrapper: Optional[pathlib.PurePath]
							 | 
						||
| 
								 | 
							
								    environ: dict
							 | 
						||
| 
								 | 
							
								    check: Callable[[], None]
							 | 
						||
| 
								 | 
							
								    # Used for build_emports().
							 | 
						||
| 
								 | 
							
								    ports: Optional[pathlib.PurePath]
							 | 
						||
| 
								 | 
							
								    cc: Optional[pathlib.PurePath]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def getenv(self, profile: "BuildProfile") -> dict:
							 | 
						||
| 
								 | 
							
								        return self.environ.copy()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def _check_clean_src():
							 | 
						||
| 
								 | 
							
								    candidates = [
							 | 
						||
| 
								 | 
							
								        SRCDIR / "Programs" / "python.o",
							 | 
						||
| 
								 | 
							
								        SRCDIR / "Python" / "frozen_modules" / "importlib._bootstrap.h",
							 | 
						||
| 
								 | 
							
								    ]
							 | 
						||
| 
								 | 
							
								    for candidate in candidates:
							 | 
						||
| 
								 | 
							
								        if candidate.exists():
							 | 
						||
| 
								 | 
							
								            raise DirtySourceDirectory(os.fspath(candidate), CLEAN_SRCDIR)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def _check_native():
							 | 
						||
| 
								 | 
							
								    if not any(shutil.which(cc) for cc in ["cc", "gcc", "clang"]):
							 | 
						||
| 
								 | 
							
								        raise MissingDependency("cc", INSTALL_NATIVE)
							 | 
						||
| 
								 | 
							
								    if not shutil.which("make"):
							 | 
						||
| 
								 | 
							
								        raise MissingDependency("make", INSTALL_NATIVE)
							 | 
						||
| 
								 | 
							
								    if sys.platform == "linux":
							 | 
						||
| 
								 | 
							
								        # skip pkg-config check on macOS
							 | 
						||
| 
								 | 
							
								        if not shutil.which("pkg-config"):
							 | 
						||
| 
								 | 
							
								            raise MissingDependency("pkg-config", INSTALL_NATIVE)
							 | 
						||
| 
								 | 
							
								        # zlib is needed to create zip files
							 | 
						||
| 
								 | 
							
								        for devel in ["zlib"]:
							 | 
						||
| 
								 | 
							
								            try:
							 | 
						||
| 
								 | 
							
								                subprocess.check_call(["pkg-config", "--exists", devel])
							 | 
						||
| 
								 | 
							
								            except subprocess.CalledProcessError:
							 | 
						||
| 
								 | 
							
								                raise MissingDependency(devel, INSTALL_NATIVE) from None
							 | 
						||
| 
								 | 
							
								    _check_clean_src()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								NATIVE = Platform(
							 | 
						||
| 
								 | 
							
								    "native",
							 | 
						||
| 
								 | 
							
								    # macOS has python.exe
							 | 
						||
| 
								 | 
							
								    pythonexe=sysconfig.get_config_var("BUILDPYTHON") or "python",
							 | 
						||
| 
								 | 
							
								    config_site=None,
							 | 
						||
| 
								 | 
							
								    configure_wrapper=None,
							 | 
						||
| 
								 | 
							
								    ports=None,
							 | 
						||
| 
								 | 
							
								    cc=None,
							 | 
						||
| 
								 | 
							
								    make_wrapper=None,
							 | 
						||
| 
								 | 
							
								    environ={},
							 | 
						||
| 
								 | 
							
								    check=_check_native,
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def _check_emscripten():
							 | 
						||
| 
								 | 
							
								    if EMSCRIPTEN_ROOT is _MISSING:
							 | 
						||
| 
								 | 
							
								        raise MissingDependency("Emscripten SDK EM_CONFIG", INSTALL_EMSDK)
							 | 
						||
| 
								 | 
							
								    # sanity check
							 | 
						||
| 
								 | 
							
								    emconfigure = EMSCRIPTEN.configure_wrapper
							 | 
						||
| 
								 | 
							
								    if not emconfigure.exists():
							 | 
						||
| 
								 | 
							
								        raise MissingDependency(os.fspath(emconfigure), INSTALL_EMSDK)
							 | 
						||
| 
								 | 
							
								    # version check
							 | 
						||
| 
								 | 
							
								    version_txt = EMSCRIPTEN_ROOT / "emscripten-version.txt"
							 | 
						||
| 
								 | 
							
								    if not version_txt.exists():
							 | 
						||
| 
								 | 
							
								        raise MissingDependency(os.fspath(version_txt), INSTALL_EMSDK)
							 | 
						||
| 
								 | 
							
								    with open(version_txt) as f:
							 | 
						||
| 
								 | 
							
								        version = f.read().strip().strip('"')
							 | 
						||
| 
								 | 
							
								    if version.endswith("-git"):
							 | 
						||
| 
								 | 
							
								        # git / upstream / tot-upstream installation
							 | 
						||
| 
								 | 
							
								        version = version[:-4]
							 | 
						||
| 
								 | 
							
								    version_tuple = tuple(int(v) for v in version.split("."))
							 | 
						||
| 
								 | 
							
								    if version_tuple < EMSDK_MIN_VERSION:
							 | 
						||
| 
								 | 
							
								        raise ConditionError(
							 | 
						||
| 
								 | 
							
								            os.fspath(version_txt),
							 | 
						||
| 
								 | 
							
								            f"Emscripten SDK {version} in '{EMSCRIPTEN_ROOT}' is older than "
							 | 
						||
| 
								 | 
							
								            "minimum required version "
							 | 
						||
| 
								 | 
							
								            f"{'.'.join(str(v) for v in EMSDK_MIN_VERSION)}.",
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								    broken = EMSDK_BROKEN_VERSION.get(version_tuple)
							 | 
						||
| 
								 | 
							
								    if broken is not None:
							 | 
						||
| 
								 | 
							
								        raise ConditionError(
							 | 
						||
| 
								 | 
							
								            os.fspath(version_txt),
							 | 
						||
| 
								 | 
							
								            (
							 | 
						||
| 
								 | 
							
								                f"Emscripten SDK {version} in '{EMSCRIPTEN_ROOT}' has known "
							 | 
						||
| 
								 | 
							
								                f"bugs, see {broken}."
							 | 
						||
| 
								 | 
							
								            ),
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								    if os.environ.get("PKG_CONFIG_PATH"):
							 | 
						||
| 
								 | 
							
								        warnings.warn(
							 | 
						||
| 
								 | 
							
								            "PKG_CONFIG_PATH is set and not empty. emconfigure overrides "
							 | 
						||
| 
								 | 
							
								            "this environment variable. Use EM_PKG_CONFIG_PATH instead."
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								    _check_clean_src()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								EMSCRIPTEN = Platform(
							 | 
						||
| 
								 | 
							
								    "emscripten",
							 | 
						||
| 
								 | 
							
								    pythonexe="python.js",
							 | 
						||
| 
								 | 
							
								    config_site=WASMTOOLS / "config.site-wasm32-emscripten",
							 | 
						||
| 
								 | 
							
								    configure_wrapper=EMSCRIPTEN_ROOT / "emconfigure",
							 | 
						||
| 
								 | 
							
								    ports=EMSCRIPTEN_ROOT / "embuilder",
							 | 
						||
| 
								 | 
							
								    cc=EMSCRIPTEN_ROOT / "emcc",
							 | 
						||
| 
								 | 
							
								    make_wrapper=EMSCRIPTEN_ROOT / "emmake",
							 | 
						||
| 
								 | 
							
								    environ={
							 | 
						||
| 
								 | 
							
								        # workaround for https://github.com/emscripten-core/emscripten/issues/17635
							 | 
						||
| 
								 | 
							
								        "TZ": "UTC",
							 | 
						||
| 
								 | 
							
								        "EM_COMPILER_WRAPPER": "ccache" if HAS_CCACHE else None,
							 | 
						||
| 
								 | 
							
								        "PATH": [EMSCRIPTEN_ROOT, os.environ["PATH"]],
							 | 
						||
| 
								 | 
							
								    },
							 | 
						||
| 
								 | 
							
								    check=_check_emscripten,
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def _check_wasi():
							 | 
						||
| 
								 | 
							
								    wasm_ld = WASI_SDK_PATH / "bin" / "wasm-ld"
							 | 
						||
| 
								 | 
							
								    if not wasm_ld.exists():
							 | 
						||
| 
								 | 
							
								        raise MissingDependency(os.fspath(wasm_ld), INSTALL_WASI_SDK)
							 | 
						||
| 
								 | 
							
								    wasmtime = shutil.which("wasmtime")
							 | 
						||
| 
								 | 
							
								    if wasmtime is None:
							 | 
						||
| 
								 | 
							
								        raise MissingDependency("wasmtime", INSTALL_WASMTIME)
							 | 
						||
| 
								 | 
							
								    _check_clean_src()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								WASI = Platform(
							 | 
						||
| 
								 | 
							
								    "wasi",
							 | 
						||
| 
								 | 
							
								    pythonexe="python.wasm",
							 | 
						||
| 
								 | 
							
								    config_site=WASMTOOLS / "config.site-wasm32-wasi",
							 | 
						||
| 
								 | 
							
								    configure_wrapper=WASMTOOLS / "wasi-env",
							 | 
						||
| 
								 | 
							
								    ports=None,
							 | 
						||
| 
								 | 
							
								    cc=WASI_SDK_PATH / "bin" / "clang",
							 | 
						||
| 
								 | 
							
								    make_wrapper=None,
							 | 
						||
| 
								 | 
							
								    environ={
							 | 
						||
| 
								 | 
							
								        "WASI_SDK_PATH": WASI_SDK_PATH,
							 | 
						||
| 
								 | 
							
								        # workaround for https://github.com/python/cpython/issues/95952
							 | 
						||
| 
								 | 
							
								        "HOSTRUNNER": (
							 | 
						||
| 
								 | 
							
								            "wasmtime run "
							 | 
						||
| 
								 | 
							
								            "--env PYTHONPATH=/{relbuilddir}/build/lib.wasi-wasm32-{version}:/Lib "
							 | 
						||
| 
								 | 
							
								            "--mapdir /::{srcdir} --"
							 | 
						||
| 
								 | 
							
								        ),
							 | 
						||
| 
								 | 
							
								        "PATH": [WASI_SDK_PATH / "bin", os.environ["PATH"]],
							 | 
						||
| 
								 | 
							
								    },
							 | 
						||
| 
								 | 
							
								    check=_check_wasi,
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class Host(enum.Enum):
							 | 
						||
| 
								 | 
							
								    """Target host triplet"""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    wasm32_emscripten = "wasm32-unknown-emscripten"
							 | 
						||
| 
								 | 
							
								    wasm64_emscripten = "wasm64-unknown-emscripten"
							 | 
						||
| 
								 | 
							
								    wasm32_wasi = "wasm32-unknown-wasi"
							 | 
						||
| 
								 | 
							
								    wasm64_wasi = "wasm64-unknown-wasi"
							 | 
						||
| 
								 | 
							
								    # current platform
							 | 
						||
| 
								 | 
							
								    build = sysconfig.get_config_var("BUILD_GNU_TYPE")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def platform(self) -> Platform:
							 | 
						||
| 
								 | 
							
								        if self.is_emscripten:
							 | 
						||
| 
								 | 
							
								            return EMSCRIPTEN
							 | 
						||
| 
								 | 
							
								        elif self.is_wasi:
							 | 
						||
| 
								 | 
							
								            return WASI
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            return NATIVE
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def is_emscripten(self) -> bool:
							 | 
						||
| 
								 | 
							
								        cls = type(self)
							 | 
						||
| 
								 | 
							
								        return self in {cls.wasm32_emscripten, cls.wasm64_emscripten}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def is_wasi(self) -> bool:
							 | 
						||
| 
								 | 
							
								        cls = type(self)
							 | 
						||
| 
								 | 
							
								        return self in {cls.wasm32_wasi, cls.wasm64_wasi}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def get_extra_paths(self) -> Iterable[pathlib.PurePath]:
							 | 
						||
| 
								 | 
							
								        """Host-specific os.environ["PATH"] entries.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        Emscripten's Node version 14.x works well for wasm32-emscripten.
							 | 
						||
| 
								 | 
							
								        wasm64-emscripten requires more recent v8 version, e.g. node 16.x.
							 | 
						||
| 
								 | 
							
								        Attempt to use system's node command.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        cls = type(self)
							 | 
						||
| 
								 | 
							
								        if self == cls.wasm32_emscripten:
							 | 
						||
| 
								 | 
							
								            return [NODE_JS.parent]
							 | 
						||
| 
								 | 
							
								        elif self == cls.wasm64_emscripten:
							 | 
						||
| 
								 | 
							
								            # TODO: look for recent node
							 | 
						||
| 
								 | 
							
								            return []
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            return []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def emport_args(self) -> List[str]:
							 | 
						||
| 
								 | 
							
								        """Host-specific port args (Emscripten)."""
							 | 
						||
| 
								 | 
							
								        cls = type(self)
							 | 
						||
| 
								 | 
							
								        if self is cls.wasm64_emscripten:
							 | 
						||
| 
								 | 
							
								            return ["-sMEMORY64=1"]
							 | 
						||
| 
								 | 
							
								        elif self is cls.wasm32_emscripten:
							 | 
						||
| 
								 | 
							
								            return ["-sMEMORY64=0"]
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            return []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def embuilder_args(self) -> List[str]:
							 | 
						||
| 
								 | 
							
								        """Host-specific embuilder args (Emscripten)."""
							 | 
						||
| 
								 | 
							
								        cls = type(self)
							 | 
						||
| 
								 | 
							
								        if self is cls.wasm64_emscripten:
							 | 
						||
| 
								 | 
							
								            return ["--wasm64"]
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            return []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class EmscriptenTarget(enum.Enum):
							 | 
						||
| 
								 | 
							
								    """Emscripten-specific targets (--with-emscripten-target)"""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    browser = "browser"
							 | 
						||
| 
								 | 
							
								    browser_debug = "browser-debug"
							 | 
						||
| 
								 | 
							
								    node = "node"
							 | 
						||
| 
								 | 
							
								    node_debug = "node-debug"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def is_browser(self):
							 | 
						||
| 
								 | 
							
								        cls = type(self)
							 | 
						||
| 
								 | 
							
								        return self in {cls.browser, cls.browser_debug}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def emport_args(self) -> List[str]:
							 | 
						||
| 
								 | 
							
								        """Target-specific port args."""
							 | 
						||
| 
								 | 
							
								        cls = type(self)
							 | 
						||
| 
								 | 
							
								        if self in {cls.browser_debug, cls.node_debug}:
							 | 
						||
| 
								 | 
							
								            # some libs come in debug and non-debug builds
							 | 
						||
| 
								 | 
							
								            return ["-O0"]
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            return ["-O2"]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class SupportLevel(enum.Enum):
							 | 
						||
| 
								 | 
							
								    supported = "tier 3, supported"
							 | 
						||
| 
								 | 
							
								    working = "working, unsupported"
							 | 
						||
| 
								 | 
							
								    experimental = "experimental, may be broken"
							 | 
						||
| 
								 | 
							
								    broken = "broken / unavailable"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def __bool__(self):
							 | 
						||
| 
								 | 
							
								        cls = type(self)
							 | 
						||
| 
								 | 
							
								        return self in {cls.supported, cls.working}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								@dataclasses.dataclass
							 | 
						||
| 
								 | 
							
								class BuildProfile:
							 | 
						||
| 
								 | 
							
								    name: str
							 | 
						||
| 
								 | 
							
								    support_level: SupportLevel
							 | 
						||
| 
								 | 
							
								    host: Host
							 | 
						||
| 
								 | 
							
								    target: Union[EmscriptenTarget, None] = None
							 | 
						||
| 
								 | 
							
								    dynamic_linking: Union[bool, None] = None
							 | 
						||
| 
								 | 
							
								    pthreads: Union[bool, None] = None
							 | 
						||
| 
								 | 
							
								    default_testopts: str = "-j2"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def is_browser(self) -> bool:
							 | 
						||
| 
								 | 
							
								        """Is this a browser build?"""
							 | 
						||
| 
								 | 
							
								        return self.target is not None and self.target.is_browser
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def builddir(self) -> pathlib.Path:
							 | 
						||
| 
								 | 
							
								        """Path to build directory"""
							 | 
						||
| 
								 | 
							
								        return BUILDDIR / self.name
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def python_cmd(self) -> pathlib.Path:
							 | 
						||
| 
								 | 
							
								        """Path to python executable"""
							 | 
						||
| 
								 | 
							
								        return self.builddir / self.host.platform.pythonexe
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def makefile(self) -> pathlib.Path:
							 | 
						||
| 
								 | 
							
								        """Path to Makefile"""
							 | 
						||
| 
								 | 
							
								        return self.builddir / "Makefile"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def configure_cmd(self) -> List[str]:
							 | 
						||
| 
								 | 
							
								        """Generate configure command"""
							 | 
						||
| 
								 | 
							
								        # use relative path, so WASI tests can find lib prefix.
							 | 
						||
| 
								 | 
							
								        # pathlib.Path.relative_to() does not work here.
							 | 
						||
| 
								 | 
							
								        configure = os.path.relpath(CONFIGURE, self.builddir)
							 | 
						||
| 
								 | 
							
								        cmd = [configure, "-C"]
							 | 
						||
| 
								 | 
							
								        platform = self.host.platform
							 | 
						||
| 
								 | 
							
								        if platform.configure_wrapper:
							 | 
						||
| 
								 | 
							
								            cmd.insert(0, os.fspath(platform.configure_wrapper))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        cmd.append(f"--host={self.host.value}")
							 | 
						||
| 
								 | 
							
								        cmd.append(f"--build={Host.build.value}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if self.target is not None:
							 | 
						||
| 
								 | 
							
								            assert self.host.is_emscripten
							 | 
						||
| 
								 | 
							
								            cmd.append(f"--with-emscripten-target={self.target.value}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if self.dynamic_linking is not None:
							 | 
						||
| 
								 | 
							
								            assert self.host.is_emscripten
							 | 
						||
| 
								 | 
							
								            opt = "enable" if self.dynamic_linking else "disable"
							 | 
						||
| 
								 | 
							
								            cmd.append(f"--{opt}-wasm-dynamic-linking")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if self.pthreads is not None:
							 | 
						||
| 
								 | 
							
								            assert self.host.is_emscripten
							 | 
						||
| 
								 | 
							
								            opt = "enable" if self.pthreads else "disable"
							 | 
						||
| 
								 | 
							
								            cmd.append(f"--{opt}-wasm-pthreads")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if self.host != Host.build:
							 | 
						||
| 
								 | 
							
								            cmd.append(f"--with-build-python={BUILD.python_cmd}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if platform.config_site is not None:
							 | 
						||
| 
								 | 
							
								            cmd.append(f"CONFIG_SITE={platform.config_site}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return cmd
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def make_cmd(self) -> List[str]:
							 | 
						||
| 
								 | 
							
								        """Generate make command"""
							 | 
						||
| 
								 | 
							
								        cmd = ["make"]
							 | 
						||
| 
								 | 
							
								        platform = self.host.platform
							 | 
						||
| 
								 | 
							
								        if platform.make_wrapper:
							 | 
						||
| 
								 | 
							
								            cmd.insert(0, os.fspath(platform.make_wrapper))
							 | 
						||
| 
								 | 
							
								        return cmd
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def getenv(self) -> dict:
							 | 
						||
| 
								 | 
							
								        """Generate environ dict for platform"""
							 | 
						||
| 
								 | 
							
								        env = os.environ.copy()
							 | 
						||
| 
								 | 
							
								        env.setdefault("MAKEFLAGS", f"-j{os.cpu_count()}")
							 | 
						||
| 
								 | 
							
								        platenv = self.host.platform.getenv(self)
							 | 
						||
| 
								 | 
							
								        for key, value in platenv.items():
							 | 
						||
| 
								 | 
							
								            if value is None:
							 | 
						||
| 
								 | 
							
								                env.pop(key, None)
							 | 
						||
| 
								 | 
							
								            elif key == "PATH":
							 | 
						||
| 
								 | 
							
								                # list of path items, prefix with extra paths
							 | 
						||
| 
								 | 
							
								                new_path: List[pathlib.PurePath] = []
							 | 
						||
| 
								 | 
							
								                new_path.extend(self.host.get_extra_paths())
							 | 
						||
| 
								 | 
							
								                new_path.extend(value)
							 | 
						||
| 
								 | 
							
								                env[key] = os.pathsep.join(os.fspath(p) for p in new_path)
							 | 
						||
| 
								 | 
							
								            elif isinstance(value, str):
							 | 
						||
| 
								 | 
							
								                env[key] = value.format(
							 | 
						||
| 
								 | 
							
								                    relbuilddir=self.builddir.relative_to(SRCDIR),
							 | 
						||
| 
								 | 
							
								                    srcdir=SRCDIR,
							 | 
						||
| 
								 | 
							
								                    version=PYTHON_VERSION,
							 | 
						||
| 
								 | 
							
								                )
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                env[key] = value
							 | 
						||
| 
								 | 
							
								        return env
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def _run_cmd(
							 | 
						||
| 
								 | 
							
								        self,
							 | 
						||
| 
								 | 
							
								        cmd: Iterable[str],
							 | 
						||
| 
								 | 
							
								        args: Iterable[str] = (),
							 | 
						||
| 
								 | 
							
								        cwd: Optional[pathlib.Path] = None,
							 | 
						||
| 
								 | 
							
								    ):
							 | 
						||
| 
								 | 
							
								        cmd = list(cmd)
							 | 
						||
| 
								 | 
							
								        cmd.extend(args)
							 | 
						||
| 
								 | 
							
								        if cwd is None:
							 | 
						||
| 
								 | 
							
								            cwd = self.builddir
							 | 
						||
| 
								 | 
							
								        logger.info('Running "%s" in "%s"', shlex.join(cmd), cwd)
							 | 
						||
| 
								 | 
							
								        return subprocess.check_call(
							 | 
						||
| 
								 | 
							
								            cmd,
							 | 
						||
| 
								 | 
							
								            cwd=os.fspath(cwd),
							 | 
						||
| 
								 | 
							
								            env=self.getenv(),
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def _check_execute(self):
							 | 
						||
| 
								 | 
							
								        if self.is_browser:
							 | 
						||
| 
								 | 
							
								            raise ValueError(f"Cannot execute on {self.target}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def run_build(self, *args):
							 | 
						||
| 
								 | 
							
								        """Run configure (if necessary) and make"""
							 | 
						||
| 
								 | 
							
								        if not self.makefile.exists():
							 | 
						||
| 
								 | 
							
								            logger.info("Makefile not found, running configure")
							 | 
						||
| 
								 | 
							
								            self.run_configure(*args)
							 | 
						||
| 
								 | 
							
								        self.run_make("all", *args)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def run_configure(self, *args):
							 | 
						||
| 
								 | 
							
								        """Run configure script to generate Makefile"""
							 | 
						||
| 
								 | 
							
								        os.makedirs(self.builddir, exist_ok=True)
							 | 
						||
| 
								 | 
							
								        return self._run_cmd(self.configure_cmd, args)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def run_make(self, *args):
							 | 
						||
| 
								 | 
							
								        """Run make (defaults to build all)"""
							 | 
						||
| 
								 | 
							
								        return self._run_cmd(self.make_cmd, args)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def run_pythoninfo(self, *args):
							 | 
						||
| 
								 | 
							
								        """Run 'make pythoninfo'"""
							 | 
						||
| 
								 | 
							
								        self._check_execute()
							 | 
						||
| 
								 | 
							
								        return self.run_make("pythoninfo", *args)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def run_test(self, target: str, testopts: Optional[str] = None):
							 | 
						||
| 
								 | 
							
								        """Run buildbottests"""
							 | 
						||
| 
								 | 
							
								        self._check_execute()
							 | 
						||
| 
								 | 
							
								        if testopts is None:
							 | 
						||
| 
								 | 
							
								            testopts = self.default_testopts
							 | 
						||
| 
								 | 
							
								        return self.run_make(target, f"TESTOPTS={testopts}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def run_py(self, *args):
							 | 
						||
| 
								 | 
							
								        """Run Python with hostrunner"""
							 | 
						||
| 
								 | 
							
								        self._check_execute()
							 | 
						||
| 
								 | 
							
								        self.run_make(
							 | 
						||
| 
								 | 
							
								            "--eval", f"run: all; $(HOSTRUNNER) ./$(PYTHON) {shlex.join(args)}", "run"
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def run_browser(self, bind="127.0.0.1", port=8000):
							 | 
						||
| 
								 | 
							
								        """Run WASM webserver and open build in browser"""
							 | 
						||
| 
								 | 
							
								        relbuilddir = self.builddir.relative_to(SRCDIR)
							 | 
						||
| 
								 | 
							
								        url = f"http://{bind}:{port}/{relbuilddir}/python.html"
							 | 
						||
| 
								 | 
							
								        args = [
							 | 
						||
| 
								 | 
							
								            sys.executable,
							 | 
						||
| 
								 | 
							
								            os.fspath(WASM_WEBSERVER),
							 | 
						||
| 
								 | 
							
								            "--bind",
							 | 
						||
| 
								 | 
							
								            bind,
							 | 
						||
| 
								 | 
							
								            "--port",
							 | 
						||
| 
								 | 
							
								            str(port),
							 | 
						||
| 
								 | 
							
								        ]
							 | 
						||
| 
								 | 
							
								        srv = subprocess.Popen(args, cwd=SRCDIR)
							 | 
						||
| 
								 | 
							
								        # wait for server
							 | 
						||
| 
								 | 
							
								        end = time.monotonic() + 3.0
							 | 
						||
| 
								 | 
							
								        while time.monotonic() < end and srv.returncode is None:
							 | 
						||
| 
								 | 
							
								            try:
							 | 
						||
| 
								 | 
							
								                with socket.create_connection((bind, port), timeout=0.1) as s:
							 | 
						||
| 
								 | 
							
								                    pass
							 | 
						||
| 
								 | 
							
								            except OSError:
							 | 
						||
| 
								 | 
							
								                time.sleep(0.01)
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                break
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        webbrowser.open(url)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            srv.wait()
							 | 
						||
| 
								 | 
							
								        except KeyboardInterrupt:
							 | 
						||
| 
								 | 
							
								            pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def clean(self, all: bool = False):
							 | 
						||
| 
								 | 
							
								        """Clean build directory"""
							 | 
						||
| 
								 | 
							
								        if all:
							 | 
						||
| 
								 | 
							
								            if self.builddir.exists():
							 | 
						||
| 
								 | 
							
								                shutil.rmtree(self.builddir)
							 | 
						||
| 
								 | 
							
								        elif self.makefile.exists():
							 | 
						||
| 
								 | 
							
								            self.run_make("clean")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def build_emports(self, force: bool = False):
							 | 
						||
| 
								 | 
							
								        """Pre-build emscripten ports."""
							 | 
						||
| 
								 | 
							
								        platform = self.host.platform
							 | 
						||
| 
								 | 
							
								        if platform.ports is None or platform.cc is None:
							 | 
						||
| 
								 | 
							
								            raise ValueError("Need ports and CC command")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        embuilder_cmd = [os.fspath(platform.ports)]
							 | 
						||
| 
								 | 
							
								        embuilder_cmd.extend(self.host.embuilder_args)
							 | 
						||
| 
								 | 
							
								        if force:
							 | 
						||
| 
								 | 
							
								            embuilder_cmd.append("--force")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        ports_cmd = [os.fspath(platform.cc)]
							 | 
						||
| 
								 | 
							
								        ports_cmd.extend(self.host.emport_args)
							 | 
						||
| 
								 | 
							
								        if self.target:
							 | 
						||
| 
								 | 
							
								            ports_cmd.extend(self.target.emport_args)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if self.dynamic_linking:
							 | 
						||
| 
								 | 
							
								            # Trigger PIC build.
							 | 
						||
| 
								 | 
							
								            ports_cmd.append("-sMAIN_MODULE")
							 | 
						||
| 
								 | 
							
								            embuilder_cmd.append("--pic")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if self.pthreads:
							 | 
						||
| 
								 | 
							
								            # Trigger multi-threaded build.
							 | 
						||
| 
								 | 
							
								            ports_cmd.append("-sUSE_PTHREADS")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Pre-build libbz2, libsqlite3, libz, and some system libs.
							 | 
						||
| 
								 | 
							
								        ports_cmd.extend(["-sUSE_ZLIB", "-sUSE_BZIP2", "-sUSE_SQLITE3"])
							 | 
						||
| 
								 | 
							
								        # Multi-threaded sqlite3 has different suffix
							 | 
						||
| 
								 | 
							
								        embuilder_cmd.extend(
							 | 
						||
| 
								 | 
							
								            ["build", "bzip2", "sqlite3-mt" if self.pthreads else "sqlite3", "zlib"]
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self._run_cmd(embuilder_cmd, cwd=SRCDIR)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        with tempfile.TemporaryDirectory(suffix="-py-emport") as tmpdir:
							 | 
						||
| 
								 | 
							
								            tmppath = pathlib.Path(tmpdir)
							 | 
						||
| 
								 | 
							
								            main_c = tmppath / "main.c"
							 | 
						||
| 
								 | 
							
								            main_js = tmppath / "main.js"
							 | 
						||
| 
								 | 
							
								            with main_c.open("w") as f:
							 | 
						||
| 
								 | 
							
								                f.write("int main(void) { return 0; }\n")
							 | 
						||
| 
								 | 
							
								            args = [
							 | 
						||
| 
								 | 
							
								                os.fspath(main_c),
							 | 
						||
| 
								 | 
							
								                "-o",
							 | 
						||
| 
								 | 
							
								                os.fspath(main_js),
							 | 
						||
| 
								 | 
							
								            ]
							 | 
						||
| 
								 | 
							
								            self._run_cmd(ports_cmd, args, cwd=tmppath)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# native build (build Python)
							 | 
						||
| 
								 | 
							
								BUILD = BuildProfile(
							 | 
						||
| 
								 | 
							
								    "build",
							 | 
						||
| 
								 | 
							
								    support_level=SupportLevel.working,
							 | 
						||
| 
								 | 
							
								    host=Host.build,
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								_profiles = [
							 | 
						||
| 
								 | 
							
								    BUILD,
							 | 
						||
| 
								 | 
							
								    # wasm32-emscripten
							 | 
						||
| 
								 | 
							
								    BuildProfile(
							 | 
						||
| 
								 | 
							
								        "emscripten-browser",
							 | 
						||
| 
								 | 
							
								        support_level=SupportLevel.supported,
							 | 
						||
| 
								 | 
							
								        host=Host.wasm32_emscripten,
							 | 
						||
| 
								 | 
							
								        target=EmscriptenTarget.browser,
							 | 
						||
| 
								 | 
							
								        dynamic_linking=True,
							 | 
						||
| 
								 | 
							
								    ),
							 | 
						||
| 
								 | 
							
								    BuildProfile(
							 | 
						||
| 
								 | 
							
								        "emscripten-browser-debug",
							 | 
						||
| 
								 | 
							
								        support_level=SupportLevel.working,
							 | 
						||
| 
								 | 
							
								        host=Host.wasm32_emscripten,
							 | 
						||
| 
								 | 
							
								        target=EmscriptenTarget.browser_debug,
							 | 
						||
| 
								 | 
							
								        dynamic_linking=True,
							 | 
						||
| 
								 | 
							
								    ),
							 | 
						||
| 
								 | 
							
								    BuildProfile(
							 | 
						||
| 
								 | 
							
								        "emscripten-node-dl",
							 | 
						||
| 
								 | 
							
								        support_level=SupportLevel.supported,
							 | 
						||
| 
								 | 
							
								        host=Host.wasm32_emscripten,
							 | 
						||
| 
								 | 
							
								        target=EmscriptenTarget.node,
							 | 
						||
| 
								 | 
							
								        dynamic_linking=True,
							 | 
						||
| 
								 | 
							
								    ),
							 | 
						||
| 
								 | 
							
								    BuildProfile(
							 | 
						||
| 
								 | 
							
								        "emscripten-node-dl-debug",
							 | 
						||
| 
								 | 
							
								        support_level=SupportLevel.working,
							 | 
						||
| 
								 | 
							
								        host=Host.wasm32_emscripten,
							 | 
						||
| 
								 | 
							
								        target=EmscriptenTarget.node_debug,
							 | 
						||
| 
								 | 
							
								        dynamic_linking=True,
							 | 
						||
| 
								 | 
							
								    ),
							 | 
						||
| 
								 | 
							
								    BuildProfile(
							 | 
						||
| 
								 | 
							
								        "emscripten-node-pthreads",
							 | 
						||
| 
								 | 
							
								        support_level=SupportLevel.supported,
							 | 
						||
| 
								 | 
							
								        host=Host.wasm32_emscripten,
							 | 
						||
| 
								 | 
							
								        target=EmscriptenTarget.node,
							 | 
						||
| 
								 | 
							
								        pthreads=True,
							 | 
						||
| 
								 | 
							
								    ),
							 | 
						||
| 
								 | 
							
								    BuildProfile(
							 | 
						||
| 
								 | 
							
								        "emscripten-node-pthreads-debug",
							 | 
						||
| 
								 | 
							
								        support_level=SupportLevel.working,
							 | 
						||
| 
								 | 
							
								        host=Host.wasm32_emscripten,
							 | 
						||
| 
								 | 
							
								        target=EmscriptenTarget.node_debug,
							 | 
						||
| 
								 | 
							
								        pthreads=True,
							 | 
						||
| 
								 | 
							
								    ),
							 | 
						||
| 
								 | 
							
								    # Emscripten build with both pthreads and dynamic linking is crashing.
							 | 
						||
| 
								 | 
							
								    BuildProfile(
							 | 
						||
| 
								 | 
							
								        "emscripten-node-dl-pthreads-debug",
							 | 
						||
| 
								 | 
							
								        support_level=SupportLevel.broken,
							 | 
						||
| 
								 | 
							
								        host=Host.wasm32_emscripten,
							 | 
						||
| 
								 | 
							
								        target=EmscriptenTarget.node_debug,
							 | 
						||
| 
								 | 
							
								        dynamic_linking=True,
							 | 
						||
| 
								 | 
							
								        pthreads=True,
							 | 
						||
| 
								 | 
							
								    ),
							 | 
						||
| 
								 | 
							
								    # wasm64-emscripten (requires Emscripten >= 3.1.21)
							 | 
						||
| 
								 | 
							
								    BuildProfile(
							 | 
						||
| 
								 | 
							
								        "wasm64-emscripten-node-debug",
							 | 
						||
| 
								 | 
							
								        support_level=SupportLevel.experimental,
							 | 
						||
| 
								 | 
							
								        host=Host.wasm64_emscripten,
							 | 
						||
| 
								 | 
							
								        target=EmscriptenTarget.node_debug,
							 | 
						||
| 
								 | 
							
								        # MEMORY64 is not compatible with dynamic linking
							 | 
						||
| 
								 | 
							
								        dynamic_linking=False,
							 | 
						||
| 
								 | 
							
								        pthreads=False,
							 | 
						||
| 
								 | 
							
								    ),
							 | 
						||
| 
								 | 
							
								    # wasm32-wasi
							 | 
						||
| 
								 | 
							
								    BuildProfile(
							 | 
						||
| 
								 | 
							
								        "wasi",
							 | 
						||
| 
								 | 
							
								        support_level=SupportLevel.supported,
							 | 
						||
| 
								 | 
							
								        host=Host.wasm32_wasi,
							 | 
						||
| 
								 | 
							
								    ),
							 | 
						||
| 
								 | 
							
								    # no SDK available yet
							 | 
						||
| 
								 | 
							
								    # BuildProfile(
							 | 
						||
| 
								 | 
							
								    #    "wasm64-wasi",
							 | 
						||
| 
								 | 
							
								    #    support_level=SupportLevel.broken,
							 | 
						||
| 
								 | 
							
								    #    host=Host.wasm64_wasi,
							 | 
						||
| 
								 | 
							
								    # ),
							 | 
						||
| 
								 | 
							
								]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								PROFILES = {p.name: p for p in _profiles}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								parser = argparse.ArgumentParser(
							 | 
						||
| 
								 | 
							
								    "wasm_build.py",
							 | 
						||
| 
								 | 
							
								    description=__doc__,
							 | 
						||
| 
								 | 
							
								    formatter_class=argparse.RawTextHelpFormatter,
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								parser.add_argument(
							 | 
						||
| 
								 | 
							
								    "--clean",
							 | 
						||
| 
								 | 
							
								    "-c",
							 | 
						||
| 
								 | 
							
								    help="Clean build directories first",
							 | 
						||
| 
								 | 
							
								    action="store_true",
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								parser.add_argument(
							 | 
						||
| 
								 | 
							
								    "--verbose",
							 | 
						||
| 
								 | 
							
								    "-v",
							 | 
						||
| 
								 | 
							
								    help="Verbose logging",
							 | 
						||
| 
								 | 
							
								    action="store_true",
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								parser.add_argument(
							 | 
						||
| 
								 | 
							
								    "--silent",
							 | 
						||
| 
								 | 
							
								    help="Run configure and make in silent mode",
							 | 
						||
| 
								 | 
							
								    action="store_true",
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								parser.add_argument(
							 | 
						||
| 
								 | 
							
								    "--testopts",
							 | 
						||
| 
								 | 
							
								    help=(
							 | 
						||
| 
								 | 
							
								        "Additional test options for 'test' and 'hostrunnertest', e.g. "
							 | 
						||
| 
								 | 
							
								        "--testopts='-v test_os'."
							 | 
						||
| 
								 | 
							
								    ),
							 | 
						||
| 
								 | 
							
								    default=None,
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Don't list broken and experimental variants in help
							 | 
						||
| 
								 | 
							
								platforms_choices = list(p.name for p in _profiles) + ["cleanall"]
							 | 
						||
| 
								 | 
							
								platforms_help = list(p.name for p in _profiles if p.support_level) + ["cleanall"]
							 | 
						||
| 
								 | 
							
								parser.add_argument(
							 | 
						||
| 
								 | 
							
								    "platform",
							 | 
						||
| 
								 | 
							
								    metavar="PLATFORM",
							 | 
						||
| 
								 | 
							
								    help=f"Build platform: {', '.join(platforms_help)}",
							 | 
						||
| 
								 | 
							
								    choices=platforms_choices,
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								ops = dict(
							 | 
						||
| 
								 | 
							
								    build="auto build (build 'build' Python, emports, configure, compile)",
							 | 
						||
| 
								 | 
							
								    configure="run ./configure",
							 | 
						||
| 
								 | 
							
								    compile="run 'make all'",
							 | 
						||
| 
								 | 
							
								    pythoninfo="run 'make pythoninfo'",
							 | 
						||
| 
								 | 
							
								    test="run 'make buildbottest TESTOPTS=...' (supports parallel tests)",
							 | 
						||
| 
								 | 
							
								    hostrunnertest="run 'make hostrunnertest TESTOPTS=...'",
							 | 
						||
| 
								 | 
							
								    repl="start interactive REPL / webserver + browser session",
							 | 
						||
| 
								 | 
							
								    clean="run 'make clean'",
							 | 
						||
| 
								 | 
							
								    cleanall="remove all build directories",
							 | 
						||
| 
								 | 
							
								    emports="build Emscripten port with embuilder (only Emscripten)",
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								ops_help = "\n".join(f"{op:16s} {help}" for op, help in ops.items())
							 | 
						||
| 
								 | 
							
								parser.add_argument(
							 | 
						||
| 
								 | 
							
								    "ops",
							 | 
						||
| 
								 | 
							
								    metavar="OP",
							 | 
						||
| 
								 | 
							
								    help=f"operation (default: build)\n\n{ops_help}",
							 | 
						||
| 
								 | 
							
								    choices=tuple(ops),
							 | 
						||
| 
								 | 
							
								    default="build",
							 | 
						||
| 
								 | 
							
								    nargs="*",
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def main():
							 | 
						||
| 
								 | 
							
								    args = parser.parse_args()
							 | 
						||
| 
								 | 
							
								    logging.basicConfig(
							 | 
						||
| 
								 | 
							
								        level=logging.INFO if args.verbose else logging.ERROR,
							 | 
						||
| 
								 | 
							
								        format="%(message)s",
							 | 
						||
| 
								 | 
							
								    )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if args.platform == "cleanall":
							 | 
						||
| 
								 | 
							
								        for builder in PROFILES.values():
							 | 
						||
| 
								 | 
							
								            builder.clean(all=True)
							 | 
						||
| 
								 | 
							
								        parser.exit(0)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # additional configure and make args
							 | 
						||
| 
								 | 
							
								    cm_args = ("--silent",) if args.silent else ()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # nargs=* with default quirk
							 | 
						||
| 
								 | 
							
								    if args.ops == "build":
							 | 
						||
| 
								 | 
							
								        args.ops = ["build"]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    builder = PROFILES[args.platform]
							 | 
						||
| 
								 | 
							
								    try:
							 | 
						||
| 
								 | 
							
								        builder.host.platform.check()
							 | 
						||
| 
								 | 
							
								    except ConditionError as e:
							 | 
						||
| 
								 | 
							
								        parser.error(str(e))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if args.clean:
							 | 
						||
| 
								 | 
							
								        builder.clean(all=False)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # hack for WASI
							 | 
						||
| 
								 | 
							
								    if builder.host.is_wasi and not SETUP_LOCAL.exists():
							 | 
						||
| 
								 | 
							
								        SETUP_LOCAL.touch()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # auto-build
							 | 
						||
| 
								 | 
							
								    if "build" in args.ops:
							 | 
						||
| 
								 | 
							
								        # check and create build Python
							 | 
						||
| 
								 | 
							
								        if builder is not BUILD:
							 | 
						||
| 
								 | 
							
								            logger.info("Auto-building 'build' Python.")
							 | 
						||
| 
								 | 
							
								            try:
							 | 
						||
| 
								 | 
							
								                BUILD.host.platform.check()
							 | 
						||
| 
								 | 
							
								            except ConditionError as e:
							 | 
						||
| 
								 | 
							
								                parser.error(str(e))
							 | 
						||
| 
								 | 
							
								            if args.clean:
							 | 
						||
| 
								 | 
							
								                BUILD.clean(all=False)
							 | 
						||
| 
								 | 
							
								            BUILD.run_build(*cm_args)
							 | 
						||
| 
								 | 
							
								        # build Emscripten ports with embuilder
							 | 
						||
| 
								 | 
							
								        if builder.host.is_emscripten and "emports" not in args.ops:
							 | 
						||
| 
								 | 
							
								            builder.build_emports()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for op in args.ops:
							 | 
						||
| 
								 | 
							
								        logger.info("\n*** %s %s", args.platform, op)
							 | 
						||
| 
								 | 
							
								        if op == "build":
							 | 
						||
| 
								 | 
							
								            builder.run_build(*cm_args)
							 | 
						||
| 
								 | 
							
								        elif op == "configure":
							 | 
						||
| 
								 | 
							
								            builder.run_configure(*cm_args)
							 | 
						||
| 
								 | 
							
								        elif op == "compile":
							 | 
						||
| 
								 | 
							
								            builder.run_make("all", *cm_args)
							 | 
						||
| 
								 | 
							
								        elif op == "pythoninfo":
							 | 
						||
| 
								 | 
							
								            builder.run_pythoninfo(*cm_args)
							 | 
						||
| 
								 | 
							
								        elif op == "repl":
							 | 
						||
| 
								 | 
							
								            if builder.is_browser:
							 | 
						||
| 
								 | 
							
								                builder.run_browser()
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                builder.run_py()
							 | 
						||
| 
								 | 
							
								        elif op == "test":
							 | 
						||
| 
								 | 
							
								            builder.run_test("buildbottest", testopts=args.testopts)
							 | 
						||
| 
								 | 
							
								        elif op == "hostrunnertest":
							 | 
						||
| 
								 | 
							
								            builder.run_test("hostrunnertest", testopts=args.testopts)
							 | 
						||
| 
								 | 
							
								        elif op == "clean":
							 | 
						||
| 
								 | 
							
								            builder.clean(all=False)
							 | 
						||
| 
								 | 
							
								        elif op == "cleanall":
							 | 
						||
| 
								 | 
							
								            builder.clean(all=True)
							 | 
						||
| 
								 | 
							
								        elif op == "emports":
							 | 
						||
| 
								 | 
							
								            builder.build_emports(force=args.clean)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            raise ValueError(op)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    print(builder.builddir)
							 | 
						||
| 
								 | 
							
								    parser.exit(0)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								if __name__ == "__main__":
							 | 
						||
| 
								 | 
							
								    main()
							 |