mirror of
https://github.com/python/cpython.git
synced 2024-11-22 05:26:10 +01:00
6acf7776ef
Use 16 MiB stack with 40 MiB memory limit, instead of 8 MiB stack with 20 MiB memory limit.
933 lines
28 KiB
Python
Executable File
933 lines
28 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 (
|
|
cast,
|
|
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.Path("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 = """
|
|
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.Path, pathlib.Path]:
|
|
"""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(r"^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) -> None:
|
|
self.info = info
|
|
self.text = text
|
|
|
|
def __str__(self) -> str:
|
|
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.Path]
|
|
make_wrapper: Optional[pathlib.PurePath]
|
|
environ: Dict[str, Any]
|
|
check: Callable[[], None]
|
|
# Used for build_emports().
|
|
ports: Optional[pathlib.PurePath]
|
|
cc: Optional[pathlib.PurePath]
|
|
|
|
def getenv(self, profile: "BuildProfile") -> Dict[str, Any]:
|
|
return self.environ.copy()
|
|
|
|
|
|
def _check_clean_src() -> None:
|
|
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() -> None:
|
|
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() -> None:
|
|
if EMSCRIPTEN_ROOT is _MISSING:
|
|
raise MissingDependency("Emscripten SDK EM_CONFIG", INSTALL_EMSDK)
|
|
# sanity check
|
|
emconfigure = EMSCRIPTEN.configure_wrapper
|
|
if emconfigure is not None and 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 = cast(
|
|
Tuple[int, int, int],
|
|
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() -> None:
|
|
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 "
|
|
"--wasm max-wasm-stack=16777216 "
|
|
"--wasi preview2 "
|
|
"--dir {srcdir}::/ "
|
|
"--env PYTHONPATH=/{relbuilddir}/build/lib.wasi-wasm32-{version}:/Lib"
|
|
),
|
|
"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) -> bool:
|
|
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) -> bool:
|
|
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:
|
|
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[str, Any]:
|
|
"""Generate environ dict for platform"""
|
|
env = os.environ.copy()
|
|
if hasattr(os, 'process_cpu_count'):
|
|
cpu_count = os.process_cpu_count()
|
|
else:
|
|
cpu_count = os.cpu_count()
|
|
env.setdefault("MAKEFLAGS", f"-j{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,
|
|
) -> int:
|
|
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) -> None:
|
|
if self.is_browser:
|
|
raise ValueError(f"Cannot execute on {self.target}")
|
|
|
|
def run_build(self, *args: str) -> None:
|
|
"""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: str) -> int:
|
|
"""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: str) -> int:
|
|
"""Run make (defaults to build all)"""
|
|
return self._run_cmd(self.make_cmd, args)
|
|
|
|
def run_pythoninfo(self, *args: str) -> int:
|
|
"""Run 'make pythoninfo'"""
|
|
self._check_execute()
|
|
return self.run_make("pythoninfo", *args)
|
|
|
|
def run_test(self, target: str, testopts: Optional[str] = None) -> int:
|
|
"""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: str) -> int:
|
|
"""Run Python with hostrunner"""
|
|
self._check_execute()
|
|
return self.run_make(
|
|
"--eval", f"run: all; $(HOSTRUNNER) ./$(PYTHON) {shlex.join(args)}", "run"
|
|
)
|
|
|
|
def run_browser(self, bind: str = "127.0.0.1", port: int = 8000) -> None:
|
|
"""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 _:
|
|
pass
|
|
except OSError:
|
|
time.sleep(0.01)
|
|
else:
|
|
break
|
|
|
|
webbrowser.open(url)
|
|
|
|
try:
|
|
srv.wait()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
def clean(self, all: bool = False) -> None:
|
|
"""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) -> None:
|
|
"""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,
|
|
),
|
|
# wasm32-wasi-threads
|
|
BuildProfile(
|
|
"wasi-threads",
|
|
support_level=SupportLevel.experimental,
|
|
host=Host.wasm32_wasi,
|
|
pthreads=True,
|
|
),
|
|
# 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() -> None:
|
|
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()
|