1#!/usr/bin/env python3 2"""Build script for Python on WebAssembly platforms. 3 4 $ ./Tools/wasm/wasm_builder.py emscripten-browser build repl 5 $ ./Tools/wasm/wasm_builder.py emscripten-node-dl build test 6 $ ./Tools/wasm/wasm_builder.py wasi build test 7 8Primary build targets are "emscripten-node-dl" (NodeJS, dynamic linking), 9"emscripten-browser", and "wasi". 10 11Emscripten builds require a recent Emscripten SDK. The tools looks for an 12activated EMSDK environment (". /path/to/emsdk_env.sh"). System packages 13(Debian, Homebrew) are not supported. 14 15WASI builds require WASI SDK and wasmtime. The tool looks for 'WASI_SDK_PATH' 16and falls back to /opt/wasi-sdk. 17 18The 'build' Python interpreter must be rebuilt every time Python's byte code 19changes. 20 21 ./Tools/wasm/wasm_builder.py --clean build build 22 23""" 24import argparse 25import enum 26import dataclasses 27import logging 28import os 29import pathlib 30import re 31import shlex 32import shutil 33import socket 34import subprocess 35import sys 36import sysconfig 37import tempfile 38import time 39import warnings 40import webbrowser 41 42# for Python 3.8 43from typing import ( 44 cast, 45 Any, 46 Callable, 47 Dict, 48 Iterable, 49 List, 50 Optional, 51 Tuple, 52 Union, 53) 54 55logger = logging.getLogger("wasm_build") 56 57SRCDIR = pathlib.Path(__file__).parent.parent.parent.absolute() 58WASMTOOLS = SRCDIR / "Tools" / "wasm" 59BUILDDIR = SRCDIR / "builddir" 60CONFIGURE = SRCDIR / "configure" 61SETUP_LOCAL = SRCDIR / "Modules" / "Setup.local" 62 63HAS_CCACHE = shutil.which("ccache") is not None 64 65# path to WASI-SDK root 66WASI_SDK_PATH = pathlib.Path(os.environ.get("WASI_SDK_PATH", "/opt/wasi-sdk")) 67 68# path to Emscripten SDK config file. 69# auto-detect's EMSDK in /opt/emsdk without ". emsdk_env.sh". 70EM_CONFIG = pathlib.Path(os.environ.setdefault("EM_CONFIG", "/opt/emsdk/.emscripten")) 71EMSDK_MIN_VERSION = (3, 1, 19) 72EMSDK_BROKEN_VERSION = { 73 (3, 1, 14): "https://github.com/emscripten-core/emscripten/issues/17338", 74 (3, 1, 16): "https://github.com/emscripten-core/emscripten/issues/17393", 75 (3, 1, 20): "https://github.com/emscripten-core/emscripten/issues/17720", 76} 77_MISSING = pathlib.Path("MISSING") 78 79WASM_WEBSERVER = WASMTOOLS / "wasm_webserver.py" 80 81CLEAN_SRCDIR = f""" 82Builds require a clean source directory. Please use a clean checkout or 83run "make clean -C '{SRCDIR}'". 84""" 85 86INSTALL_NATIVE = """ 87Builds require a C compiler (gcc, clang), make, pkg-config, and development 88headers for dependencies like zlib. 89 90Debian/Ubuntu: sudo apt install build-essential git curl pkg-config zlib1g-dev 91Fedora/CentOS: sudo dnf install gcc make git-core curl pkgconfig zlib-devel 92""" 93 94INSTALL_EMSDK = """ 95wasm32-emscripten builds need Emscripten SDK. Please follow instructions at 96https://emscripten.org/docs/getting_started/downloads.html how to install 97Emscripten and how to activate the SDK with "emsdk_env.sh". 98 99 git clone https://github.com/emscripten-core/emsdk.git /path/to/emsdk 100 cd /path/to/emsdk 101 ./emsdk install latest 102 ./emsdk activate latest 103 source /path/to/emsdk_env.sh 104""" 105 106INSTALL_WASI_SDK = """ 107wasm32-wasi builds need WASI SDK. Please fetch the latest SDK from 108https://github.com/WebAssembly/wasi-sdk/releases and install it to 109"/opt/wasi-sdk". Alternatively you can install the SDK in a different location 110and point the environment variable WASI_SDK_PATH to the root directory 111of the SDK. The SDK is available for Linux x86_64, macOS x86_64, and MinGW. 112""" 113 114INSTALL_WASMTIME = """ 115wasm32-wasi tests require wasmtime on PATH. Please follow instructions at 116https://wasmtime.dev/ to install wasmtime. 117""" 118 119 120def parse_emconfig( 121 emconfig: pathlib.Path = EM_CONFIG, 122) -> Tuple[pathlib.Path, pathlib.Path]: 123 """Parse EM_CONFIG file and lookup EMSCRIPTEN_ROOT and NODE_JS. 124 125 The ".emscripten" config file is a Python snippet that uses "EM_CONFIG" 126 environment variable. EMSCRIPTEN_ROOT is the "upstream/emscripten" 127 subdirectory with tools like "emconfigure". 128 """ 129 if not emconfig.exists(): 130 return _MISSING, _MISSING 131 with open(emconfig, encoding="utf-8") as f: 132 code = f.read() 133 # EM_CONFIG file is a Python snippet 134 local: Dict[str, Any] = {} 135 exec(code, globals(), local) 136 emscripten_root = pathlib.Path(local["EMSCRIPTEN_ROOT"]) 137 node_js = pathlib.Path(local["NODE_JS"]) 138 return emscripten_root, node_js 139 140 141EMSCRIPTEN_ROOT, NODE_JS = parse_emconfig() 142 143 144def read_python_version(configure: pathlib.Path = CONFIGURE) -> str: 145 """Read PACKAGE_VERSION from configure script 146 147 configure and configure.ac are the canonical source for major and 148 minor version number. 149 """ 150 version_re = re.compile(r"^PACKAGE_VERSION='(\d\.\d+)'") 151 with configure.open(encoding="utf-8") as f: 152 for line in f: 153 mo = version_re.match(line) 154 if mo: 155 return mo.group(1) 156 raise ValueError(f"PACKAGE_VERSION not found in {configure}") 157 158 159PYTHON_VERSION = read_python_version() 160 161 162class ConditionError(ValueError): 163 def __init__(self, info: str, text: str) -> None: 164 self.info = info 165 self.text = text 166 167 def __str__(self) -> str: 168 return f"{type(self).__name__}: '{self.info}'\n{self.text}" 169 170 171class MissingDependency(ConditionError): 172 pass 173 174 175class DirtySourceDirectory(ConditionError): 176 pass 177 178 179@dataclasses.dataclass 180class Platform: 181 """Platform-specific settings 182 183 - CONFIG_SITE override 184 - configure wrapper (e.g. emconfigure) 185 - make wrapper (e.g. emmake) 186 - additional environment variables 187 - check function to verify SDK 188 """ 189 190 name: str 191 pythonexe: str 192 config_site: Optional[pathlib.PurePath] 193 configure_wrapper: Optional[pathlib.Path] 194 make_wrapper: Optional[pathlib.PurePath] 195 environ: Dict[str, Any] 196 check: Callable[[], None] 197 # Used for build_emports(). 198 ports: Optional[pathlib.PurePath] 199 cc: Optional[pathlib.PurePath] 200 201 def getenv(self, profile: "BuildProfile") -> Dict[str, Any]: 202 return self.environ.copy() 203 204 205def _check_clean_src() -> None: 206 candidates = [ 207 SRCDIR / "Programs" / "python.o", 208 SRCDIR / "Python" / "frozen_modules" / "importlib._bootstrap.h", 209 ] 210 for candidate in candidates: 211 if candidate.exists(): 212 raise DirtySourceDirectory(os.fspath(candidate), CLEAN_SRCDIR) 213 214 215def _check_native() -> None: 216 if not any(shutil.which(cc) for cc in ["cc", "gcc", "clang"]): 217 raise MissingDependency("cc", INSTALL_NATIVE) 218 if not shutil.which("make"): 219 raise MissingDependency("make", INSTALL_NATIVE) 220 if sys.platform == "linux": 221 # skip pkg-config check on macOS 222 if not shutil.which("pkg-config"): 223 raise MissingDependency("pkg-config", INSTALL_NATIVE) 224 # zlib is needed to create zip files 225 for devel in ["zlib"]: 226 try: 227 subprocess.check_call(["pkg-config", "--exists", devel]) 228 except subprocess.CalledProcessError: 229 raise MissingDependency(devel, INSTALL_NATIVE) from None 230 _check_clean_src() 231 232 233NATIVE = Platform( 234 "native", 235 # macOS has python.exe 236 pythonexe=sysconfig.get_config_var("BUILDPYTHON") or "python", 237 config_site=None, 238 configure_wrapper=None, 239 ports=None, 240 cc=None, 241 make_wrapper=None, 242 environ={}, 243 check=_check_native, 244) 245 246 247def _check_emscripten() -> None: 248 if EMSCRIPTEN_ROOT is _MISSING: 249 raise MissingDependency("Emscripten SDK EM_CONFIG", INSTALL_EMSDK) 250 # sanity check 251 emconfigure = EMSCRIPTEN.configure_wrapper 252 if emconfigure is not None and not emconfigure.exists(): 253 raise MissingDependency(os.fspath(emconfigure), INSTALL_EMSDK) 254 # version check 255 version_txt = EMSCRIPTEN_ROOT / "emscripten-version.txt" 256 if not version_txt.exists(): 257 raise MissingDependency(os.fspath(version_txt), INSTALL_EMSDK) 258 with open(version_txt) as f: 259 version = f.read().strip().strip('"') 260 if version.endswith("-git"): 261 # git / upstream / tot-upstream installation 262 version = version[:-4] 263 version_tuple = cast( 264 Tuple[int, int, int], 265 tuple(int(v) for v in version.split(".")) 266 ) 267 if version_tuple < EMSDK_MIN_VERSION: 268 raise ConditionError( 269 os.fspath(version_txt), 270 f"Emscripten SDK {version} in '{EMSCRIPTEN_ROOT}' is older than " 271 "minimum required version " 272 f"{'.'.join(str(v) for v in EMSDK_MIN_VERSION)}.", 273 ) 274 broken = EMSDK_BROKEN_VERSION.get(version_tuple) 275 if broken is not None: 276 raise ConditionError( 277 os.fspath(version_txt), 278 ( 279 f"Emscripten SDK {version} in '{EMSCRIPTEN_ROOT}' has known " 280 f"bugs, see {broken}." 281 ), 282 ) 283 if os.environ.get("PKG_CONFIG_PATH"): 284 warnings.warn( 285 "PKG_CONFIG_PATH is set and not empty. emconfigure overrides " 286 "this environment variable. Use EM_PKG_CONFIG_PATH instead." 287 ) 288 _check_clean_src() 289 290 291EMSCRIPTEN = Platform( 292 "emscripten", 293 pythonexe="python.js", 294 config_site=WASMTOOLS / "config.site-wasm32-emscripten", 295 configure_wrapper=EMSCRIPTEN_ROOT / "emconfigure", 296 ports=EMSCRIPTEN_ROOT / "embuilder", 297 cc=EMSCRIPTEN_ROOT / "emcc", 298 make_wrapper=EMSCRIPTEN_ROOT / "emmake", 299 environ={ 300 # workaround for https://github.com/emscripten-core/emscripten/issues/17635 301 "TZ": "UTC", 302 "EM_COMPILER_WRAPPER": "ccache" if HAS_CCACHE else None, 303 "PATH": [EMSCRIPTEN_ROOT, os.environ["PATH"]], 304 }, 305 check=_check_emscripten, 306) 307 308 309def _check_wasi() -> None: 310 wasm_ld = WASI_SDK_PATH / "bin" / "wasm-ld" 311 if not wasm_ld.exists(): 312 raise MissingDependency(os.fspath(wasm_ld), INSTALL_WASI_SDK) 313 wasmtime = shutil.which("wasmtime") 314 if wasmtime is None: 315 raise MissingDependency("wasmtime", INSTALL_WASMTIME) 316 _check_clean_src() 317 318 319WASI = Platform( 320 "wasi", 321 pythonexe="python.wasm", 322 config_site=WASMTOOLS / "config.site-wasm32-wasi", 323 configure_wrapper=WASMTOOLS / "wasi-env", 324 ports=None, 325 cc=WASI_SDK_PATH / "bin" / "clang", 326 make_wrapper=None, 327 environ={ 328 "WASI_SDK_PATH": WASI_SDK_PATH, 329 # workaround for https://github.com/python/cpython/issues/95952 330 "HOSTRUNNER": ( 331 "wasmtime run " 332 "--wasm max-wasm-stack=8388608 " 333 "--wasi preview2 " 334 "--dir {srcdir}::/ " 335 "--env PYTHONPATH=/{relbuilddir}/build/lib.wasi-wasm32-{version}:/Lib" 336 ), 337 "PATH": [WASI_SDK_PATH / "bin", os.environ["PATH"]], 338 }, 339 check=_check_wasi, 340) 341 342 343class Host(enum.Enum): 344 """Target host triplet""" 345 346 wasm32_emscripten = "wasm32-unknown-emscripten" 347 wasm64_emscripten = "wasm64-unknown-emscripten" 348 wasm32_wasi = "wasm32-unknown-wasi" 349 wasm64_wasi = "wasm64-unknown-wasi" 350 # current platform 351 build = sysconfig.get_config_var("BUILD_GNU_TYPE") 352 353 @property 354 def platform(self) -> Platform: 355 if self.is_emscripten: 356 return EMSCRIPTEN 357 elif self.is_wasi: 358 return WASI 359 else: 360 return NATIVE 361 362 @property 363 def is_emscripten(self) -> bool: 364 cls = type(self) 365 return self in {cls.wasm32_emscripten, cls.wasm64_emscripten} 366 367 @property 368 def is_wasi(self) -> bool: 369 cls = type(self) 370 return self in {cls.wasm32_wasi, cls.wasm64_wasi} 371 372 def get_extra_paths(self) -> Iterable[pathlib.PurePath]: 373 """Host-specific os.environ["PATH"] entries. 374 375 Emscripten's Node version 14.x works well for wasm32-emscripten. 376 wasm64-emscripten requires more recent v8 version, e.g. node 16.x. 377 Attempt to use system's node command. 378 """ 379 cls = type(self) 380 if self == cls.wasm32_emscripten: 381 return [NODE_JS.parent] 382 elif self == cls.wasm64_emscripten: 383 # TODO: look for recent node 384 return [] 385 else: 386 return [] 387 388 @property 389 def emport_args(self) -> List[str]: 390 """Host-specific port args (Emscripten).""" 391 cls = type(self) 392 if self is cls.wasm64_emscripten: 393 return ["-sMEMORY64=1"] 394 elif self is cls.wasm32_emscripten: 395 return ["-sMEMORY64=0"] 396 else: 397 return [] 398 399 @property 400 def embuilder_args(self) -> List[str]: 401 """Host-specific embuilder args (Emscripten).""" 402 cls = type(self) 403 if self is cls.wasm64_emscripten: 404 return ["--wasm64"] 405 else: 406 return [] 407 408 409class EmscriptenTarget(enum.Enum): 410 """Emscripten-specific targets (--with-emscripten-target)""" 411 412 browser = "browser" 413 browser_debug = "browser-debug" 414 node = "node" 415 node_debug = "node-debug" 416 417 @property 418 def is_browser(self) -> bool: 419 cls = type(self) 420 return self in {cls.browser, cls.browser_debug} 421 422 @property 423 def emport_args(self) -> List[str]: 424 """Target-specific port args.""" 425 cls = type(self) 426 if self in {cls.browser_debug, cls.node_debug}: 427 # some libs come in debug and non-debug builds 428 return ["-O0"] 429 else: 430 return ["-O2"] 431 432 433class SupportLevel(enum.Enum): 434 supported = "tier 3, supported" 435 working = "working, unsupported" 436 experimental = "experimental, may be broken" 437 broken = "broken / unavailable" 438 439 def __bool__(self) -> bool: 440 cls = type(self) 441 return self in {cls.supported, cls.working} 442 443 444@dataclasses.dataclass 445class BuildProfile: 446 name: str 447 support_level: SupportLevel 448 host: Host 449 target: Union[EmscriptenTarget, None] = None 450 dynamic_linking: Union[bool, None] = None 451 pthreads: Union[bool, None] = None 452 default_testopts: str = "-j2" 453 454 @property 455 def is_browser(self) -> bool: 456 """Is this a browser build?""" 457 return self.target is not None and self.target.is_browser 458 459 @property 460 def builddir(self) -> pathlib.Path: 461 """Path to build directory""" 462 return BUILDDIR / self.name 463 464 @property 465 def python_cmd(self) -> pathlib.Path: 466 """Path to python executable""" 467 return self.builddir / self.host.platform.pythonexe 468 469 @property 470 def makefile(self) -> pathlib.Path: 471 """Path to Makefile""" 472 return self.builddir / "Makefile" 473 474 @property 475 def configure_cmd(self) -> List[str]: 476 """Generate configure command""" 477 # use relative path, so WASI tests can find lib prefix. 478 # pathlib.Path.relative_to() does not work here. 479 configure = os.path.relpath(CONFIGURE, self.builddir) 480 cmd = [configure, "-C"] 481 platform = self.host.platform 482 if platform.configure_wrapper: 483 cmd.insert(0, os.fspath(platform.configure_wrapper)) 484 485 cmd.append(f"--host={self.host.value}") 486 cmd.append(f"--build={Host.build.value}") 487 488 if self.target is not None: 489 assert self.host.is_emscripten 490 cmd.append(f"--with-emscripten-target={self.target.value}") 491 492 if self.dynamic_linking is not None: 493 assert self.host.is_emscripten 494 opt = "enable" if self.dynamic_linking else "disable" 495 cmd.append(f"--{opt}-wasm-dynamic-linking") 496 497 if self.pthreads is not None: 498 opt = "enable" if self.pthreads else "disable" 499 cmd.append(f"--{opt}-wasm-pthreads") 500 501 if self.host != Host.build: 502 cmd.append(f"--with-build-python={BUILD.python_cmd}") 503 504 if platform.config_site is not None: 505 cmd.append(f"CONFIG_SITE={platform.config_site}") 506 507 return cmd 508 509 @property 510 def make_cmd(self) -> List[str]: 511 """Generate make command""" 512 cmd = ["make"] 513 platform = self.host.platform 514 if platform.make_wrapper: 515 cmd.insert(0, os.fspath(platform.make_wrapper)) 516 return cmd 517 518 def getenv(self) -> Dict[str, Any]: 519 """Generate environ dict for platform""" 520 env = os.environ.copy() 521 if hasattr(os, 'process_cpu_count'): 522 cpu_count = os.process_cpu_count() 523 else: 524 cpu_count = os.cpu_count() 525 env.setdefault("MAKEFLAGS", f"-j{cpu_count}") 526 platenv = self.host.platform.getenv(self) 527 for key, value in platenv.items(): 528 if value is None: 529 env.pop(key, None) 530 elif key == "PATH": 531 # list of path items, prefix with extra paths 532 new_path: List[pathlib.PurePath] = [] 533 new_path.extend(self.host.get_extra_paths()) 534 new_path.extend(value) 535 env[key] = os.pathsep.join(os.fspath(p) for p in new_path) 536 elif isinstance(value, str): 537 env[key] = value.format( 538 relbuilddir=self.builddir.relative_to(SRCDIR), 539 srcdir=SRCDIR, 540 version=PYTHON_VERSION, 541 ) 542 else: 543 env[key] = value 544 return env 545 546 def _run_cmd( 547 self, 548 cmd: Iterable[str], 549 args: Iterable[str] = (), 550 cwd: Optional[pathlib.Path] = None, 551 ) -> int: 552 cmd = list(cmd) 553 cmd.extend(args) 554 if cwd is None: 555 cwd = self.builddir 556 logger.info('Running "%s" in "%s"', shlex.join(cmd), cwd) 557 return subprocess.check_call( 558 cmd, 559 cwd=os.fspath(cwd), 560 env=self.getenv(), 561 ) 562 563 def _check_execute(self) -> None: 564 if self.is_browser: 565 raise ValueError(f"Cannot execute on {self.target}") 566 567 def run_build(self, *args: str) -> None: 568 """Run configure (if necessary) and make""" 569 if not self.makefile.exists(): 570 logger.info("Makefile not found, running configure") 571 self.run_configure(*args) 572 self.run_make("all", *args) 573 574 def run_configure(self, *args: str) -> int: 575 """Run configure script to generate Makefile""" 576 os.makedirs(self.builddir, exist_ok=True) 577 return self._run_cmd(self.configure_cmd, args) 578 579 def run_make(self, *args: str) -> int: 580 """Run make (defaults to build all)""" 581 return self._run_cmd(self.make_cmd, args) 582 583 def run_pythoninfo(self, *args: str) -> int: 584 """Run 'make pythoninfo'""" 585 self._check_execute() 586 return self.run_make("pythoninfo", *args) 587 588 def run_test(self, target: str, testopts: Optional[str] = None) -> int: 589 """Run buildbottests""" 590 self._check_execute() 591 if testopts is None: 592 testopts = self.default_testopts 593 return self.run_make(target, f"TESTOPTS={testopts}") 594 595 def run_py(self, *args: str) -> int: 596 """Run Python with hostrunner""" 597 self._check_execute() 598 return self.run_make( 599 "--eval", f"run: all; $(HOSTRUNNER) ./$(PYTHON) {shlex.join(args)}", "run" 600 ) 601 602 def run_browser(self, bind: str = "127.0.0.1", port: int = 8000) -> None: 603 """Run WASM webserver and open build in browser""" 604 relbuilddir = self.builddir.relative_to(SRCDIR) 605 url = f"http://{bind}:{port}/{relbuilddir}/python.html" 606 args = [ 607 sys.executable, 608 os.fspath(WASM_WEBSERVER), 609 "--bind", 610 bind, 611 "--port", 612 str(port), 613 ] 614 srv = subprocess.Popen(args, cwd=SRCDIR) 615 # wait for server 616 end = time.monotonic() + 3.0 617 while time.monotonic() < end and srv.returncode is None: 618 try: 619 with socket.create_connection((bind, port), timeout=0.1) as _: 620 pass 621 except OSError: 622 time.sleep(0.01) 623 else: 624 break 625 626 webbrowser.open(url) 627 628 try: 629 srv.wait() 630 except KeyboardInterrupt: 631 pass 632 633 def clean(self, all: bool = False) -> None: 634 """Clean build directory""" 635 if all: 636 if self.builddir.exists(): 637 shutil.rmtree(self.builddir) 638 elif self.makefile.exists(): 639 self.run_make("clean") 640 641 def build_emports(self, force: bool = False) -> None: 642 """Pre-build emscripten ports.""" 643 platform = self.host.platform 644 if platform.ports is None or platform.cc is None: 645 raise ValueError("Need ports and CC command") 646 647 embuilder_cmd = [os.fspath(platform.ports)] 648 embuilder_cmd.extend(self.host.embuilder_args) 649 if force: 650 embuilder_cmd.append("--force") 651 652 ports_cmd = [os.fspath(platform.cc)] 653 ports_cmd.extend(self.host.emport_args) 654 if self.target: 655 ports_cmd.extend(self.target.emport_args) 656 657 if self.dynamic_linking: 658 # Trigger PIC build. 659 ports_cmd.append("-sMAIN_MODULE") 660 embuilder_cmd.append("--pic") 661 662 if self.pthreads: 663 # Trigger multi-threaded build. 664 ports_cmd.append("-sUSE_PTHREADS") 665 666 # Pre-build libbz2, libsqlite3, libz, and some system libs. 667 ports_cmd.extend(["-sUSE_ZLIB", "-sUSE_BZIP2", "-sUSE_SQLITE3"]) 668 # Multi-threaded sqlite3 has different suffix 669 embuilder_cmd.extend( 670 ["build", "bzip2", "sqlite3-mt" if self.pthreads else "sqlite3", "zlib"] 671 ) 672 673 self._run_cmd(embuilder_cmd, cwd=SRCDIR) 674 675 with tempfile.TemporaryDirectory(suffix="-py-emport") as tmpdir: 676 tmppath = pathlib.Path(tmpdir) 677 main_c = tmppath / "main.c" 678 main_js = tmppath / "main.js" 679 with main_c.open("w") as f: 680 f.write("int main(void) { return 0; }\n") 681 args = [ 682 os.fspath(main_c), 683 "-o", 684 os.fspath(main_js), 685 ] 686 self._run_cmd(ports_cmd, args, cwd=tmppath) 687 688 689# native build (build Python) 690BUILD = BuildProfile( 691 "build", 692 support_level=SupportLevel.working, 693 host=Host.build, 694) 695 696_profiles = [ 697 BUILD, 698 # wasm32-emscripten 699 BuildProfile( 700 "emscripten-browser", 701 support_level=SupportLevel.supported, 702 host=Host.wasm32_emscripten, 703 target=EmscriptenTarget.browser, 704 dynamic_linking=True, 705 ), 706 BuildProfile( 707 "emscripten-browser-debug", 708 support_level=SupportLevel.working, 709 host=Host.wasm32_emscripten, 710 target=EmscriptenTarget.browser_debug, 711 dynamic_linking=True, 712 ), 713 BuildProfile( 714 "emscripten-node-dl", 715 support_level=SupportLevel.supported, 716 host=Host.wasm32_emscripten, 717 target=EmscriptenTarget.node, 718 dynamic_linking=True, 719 ), 720 BuildProfile( 721 "emscripten-node-dl-debug", 722 support_level=SupportLevel.working, 723 host=Host.wasm32_emscripten, 724 target=EmscriptenTarget.node_debug, 725 dynamic_linking=True, 726 ), 727 BuildProfile( 728 "emscripten-node-pthreads", 729 support_level=SupportLevel.supported, 730 host=Host.wasm32_emscripten, 731 target=EmscriptenTarget.node, 732 pthreads=True, 733 ), 734 BuildProfile( 735 "emscripten-node-pthreads-debug", 736 support_level=SupportLevel.working, 737 host=Host.wasm32_emscripten, 738 target=EmscriptenTarget.node_debug, 739 pthreads=True, 740 ), 741 # Emscripten build with both pthreads and dynamic linking is crashing. 742 BuildProfile( 743 "emscripten-node-dl-pthreads-debug", 744 support_level=SupportLevel.broken, 745 host=Host.wasm32_emscripten, 746 target=EmscriptenTarget.node_debug, 747 dynamic_linking=True, 748 pthreads=True, 749 ), 750 # wasm64-emscripten (requires Emscripten >= 3.1.21) 751 BuildProfile( 752 "wasm64-emscripten-node-debug", 753 support_level=SupportLevel.experimental, 754 host=Host.wasm64_emscripten, 755 target=EmscriptenTarget.node_debug, 756 # MEMORY64 is not compatible with dynamic linking 757 dynamic_linking=False, 758 pthreads=False, 759 ), 760 # wasm32-wasi 761 BuildProfile( 762 "wasi", 763 support_level=SupportLevel.supported, 764 host=Host.wasm32_wasi, 765 ), 766 # wasm32-wasi-threads 767 BuildProfile( 768 "wasi-threads", 769 support_level=SupportLevel.experimental, 770 host=Host.wasm32_wasi, 771 pthreads=True, 772 ), 773 # no SDK available yet 774 # BuildProfile( 775 # "wasm64-wasi", 776 # support_level=SupportLevel.broken, 777 # host=Host.wasm64_wasi, 778 # ), 779] 780 781PROFILES = {p.name: p for p in _profiles} 782 783parser = argparse.ArgumentParser( 784 "wasm_build.py", 785 description=__doc__, 786 formatter_class=argparse.RawTextHelpFormatter, 787) 788 789parser.add_argument( 790 "--clean", 791 "-c", 792 help="Clean build directories first", 793 action="store_true", 794) 795 796parser.add_argument( 797 "--verbose", 798 "-v", 799 help="Verbose logging", 800 action="store_true", 801) 802 803parser.add_argument( 804 "--silent", 805 help="Run configure and make in silent mode", 806 action="store_true", 807) 808 809parser.add_argument( 810 "--testopts", 811 help=( 812 "Additional test options for 'test' and 'hostrunnertest', e.g. " 813 "--testopts='-v test_os'." 814 ), 815 default=None, 816) 817 818# Don't list broken and experimental variants in help 819platforms_choices = list(p.name for p in _profiles) + ["cleanall"] 820platforms_help = list(p.name for p in _profiles if p.support_level) + ["cleanall"] 821parser.add_argument( 822 "platform", 823 metavar="PLATFORM", 824 help=f"Build platform: {', '.join(platforms_help)}", 825 choices=platforms_choices, 826) 827 828ops = dict( 829 build="auto build (build 'build' Python, emports, configure, compile)", 830 configure="run ./configure", 831 compile="run 'make all'", 832 pythoninfo="run 'make pythoninfo'", 833 test="run 'make buildbottest TESTOPTS=...' (supports parallel tests)", 834 hostrunnertest="run 'make hostrunnertest TESTOPTS=...'", 835 repl="start interactive REPL / webserver + browser session", 836 clean="run 'make clean'", 837 cleanall="remove all build directories", 838 emports="build Emscripten port with embuilder (only Emscripten)", 839) 840ops_help = "\n".join(f"{op:16s} {help}" for op, help in ops.items()) 841parser.add_argument( 842 "ops", 843 metavar="OP", 844 help=f"operation (default: build)\n\n{ops_help}", 845 choices=tuple(ops), 846 default="build", 847 nargs="*", 848) 849 850 851def main() -> None: 852 args = parser.parse_args() 853 logging.basicConfig( 854 level=logging.INFO if args.verbose else logging.ERROR, 855 format="%(message)s", 856 ) 857 858 if args.platform == "cleanall": 859 for builder in PROFILES.values(): 860 builder.clean(all=True) 861 parser.exit(0) 862 863 # additional configure and make args 864 cm_args = ("--silent",) if args.silent else () 865 866 # nargs=* with default quirk 867 if args.ops == "build": 868 args.ops = ["build"] 869 870 builder = PROFILES[args.platform] 871 try: 872 builder.host.platform.check() 873 except ConditionError as e: 874 parser.error(str(e)) 875 876 if args.clean: 877 builder.clean(all=False) 878 879 # hack for WASI 880 if builder.host.is_wasi and not SETUP_LOCAL.exists(): 881 SETUP_LOCAL.touch() 882 883 # auto-build 884 if "build" in args.ops: 885 # check and create build Python 886 if builder is not BUILD: 887 logger.info("Auto-building 'build' Python.") 888 try: 889 BUILD.host.platform.check() 890 except ConditionError as e: 891 parser.error(str(e)) 892 if args.clean: 893 BUILD.clean(all=False) 894 BUILD.run_build(*cm_args) 895 # build Emscripten ports with embuilder 896 if builder.host.is_emscripten and "emports" not in args.ops: 897 builder.build_emports() 898 899 for op in args.ops: 900 logger.info("\n*** %s %s", args.platform, op) 901 if op == "build": 902 builder.run_build(*cm_args) 903 elif op == "configure": 904 builder.run_configure(*cm_args) 905 elif op == "compile": 906 builder.run_make("all", *cm_args) 907 elif op == "pythoninfo": 908 builder.run_pythoninfo(*cm_args) 909 elif op == "repl": 910 if builder.is_browser: 911 builder.run_browser() 912 else: 913 builder.run_py() 914 elif op == "test": 915 builder.run_test("buildbottest", testopts=args.testopts) 916 elif op == "hostrunnertest": 917 builder.run_test("hostrunnertest", testopts=args.testopts) 918 elif op == "clean": 919 builder.clean(all=False) 920 elif op == "cleanall": 921 builder.clean(all=True) 922 elif op == "emports": 923 builder.build_emports(force=args.clean) 924 else: 925 raise ValueError(op) 926 927 print(builder.builddir) 928 parser.exit(0) 929 930 931if __name__ == "__main__": 932 main() 933