mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 03:44:55 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			907 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			907 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
#!/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()
 |