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