mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			467 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			467 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/env python3
 | ||
| 
 | ||
| import argparse
 | ||
| import contextlib
 | ||
| import functools
 | ||
| import os
 | ||
| import shutil
 | ||
| import subprocess
 | ||
| import sys
 | ||
| import sysconfig
 | ||
| import hashlib
 | ||
| import tempfile
 | ||
| from urllib.request import urlopen
 | ||
| from pathlib import Path
 | ||
| from textwrap import dedent
 | ||
| 
 | ||
| try:
 | ||
|     from os import process_cpu_count as cpu_count
 | ||
| except ImportError:
 | ||
|     from os import cpu_count
 | ||
| 
 | ||
| 
 | ||
| EMSCRIPTEN_DIR = Path(__file__).parent
 | ||
| CHECKOUT = EMSCRIPTEN_DIR.parent.parent.parent
 | ||
| 
 | ||
| CROSS_BUILD_DIR = CHECKOUT / "cross-build"
 | ||
| NATIVE_BUILD_DIR = CROSS_BUILD_DIR / "build"
 | ||
| HOST_TRIPLE = "wasm32-emscripten"
 | ||
| 
 | ||
| DOWNLOAD_DIR = CROSS_BUILD_DIR / HOST_TRIPLE / "build"
 | ||
| HOST_BUILD_DIR = CROSS_BUILD_DIR / HOST_TRIPLE / "build"
 | ||
| HOST_DIR = HOST_BUILD_DIR / "python"
 | ||
| PREFIX_DIR = CROSS_BUILD_DIR / HOST_TRIPLE / "prefix"
 | ||
| 
 | ||
| LOCAL_SETUP = CHECKOUT / "Modules" / "Setup.local"
 | ||
| LOCAL_SETUP_MARKER = "# Generated by Tools/wasm/emscripten.py\n".encode("utf-8")
 | ||
| 
 | ||
| 
 | ||
| def updated_env(updates={}):
 | ||
|     """Create a new dict representing the environment to use.
 | ||
| 
 | ||
|     The changes made to the execution environment are printed out.
 | ||
|     """
 | ||
|     env_defaults = {}
 | ||
|     # https://reproducible-builds.org/docs/source-date-epoch/
 | ||
|     git_epoch_cmd = ["git", "log", "-1", "--pretty=%ct"]
 | ||
|     try:
 | ||
|         epoch = subprocess.check_output(git_epoch_cmd, encoding="utf-8").strip()
 | ||
|         env_defaults["SOURCE_DATE_EPOCH"] = epoch
 | ||
|     except subprocess.CalledProcessError:
 | ||
|         pass  # Might be building from a tarball.
 | ||
|     # This layering lets SOURCE_DATE_EPOCH from os.environ takes precedence.
 | ||
|     environment = env_defaults | os.environ | updates
 | ||
| 
 | ||
|     env_diff = {}
 | ||
|     for key, value in environment.items():
 | ||
|         if os.environ.get(key) != value:
 | ||
|             env_diff[key] = value
 | ||
| 
 | ||
|     print("๐ Environment changes:")
 | ||
|     for key in sorted(env_diff.keys()):
 | ||
|         print(f"  {key}={env_diff[key]}")
 | ||
| 
 | ||
|     return environment
 | ||
| 
 | ||
| 
 | ||
| def subdir(working_dir, *, clean_ok=False):
 | ||
|     """Decorator to change to a working directory."""
 | ||
| 
 | ||
|     def decorator(func):
 | ||
|         @functools.wraps(func)
 | ||
|         def wrapper(context):
 | ||
|             try:
 | ||
|                 tput_output = subprocess.check_output(
 | ||
|                     ["tput", "cols"], encoding="utf-8"
 | ||
|                 )
 | ||
|                 terminal_width = int(tput_output.strip())
 | ||
|             except subprocess.CalledProcessError:
 | ||
|                 terminal_width = 80
 | ||
|             print("โฏ" * terminal_width)
 | ||
|             print("๐", working_dir)
 | ||
|             if clean_ok and getattr(context, "clean", False) and working_dir.exists():
 | ||
|                 print("๐ฎ Deleting directory (--clean)...")
 | ||
|                 shutil.rmtree(working_dir)
 | ||
| 
 | ||
|             working_dir.mkdir(parents=True, exist_ok=True)
 | ||
| 
 | ||
|             with contextlib.chdir(working_dir):
 | ||
|                 return func(context, working_dir)
 | ||
| 
 | ||
|         return wrapper
 | ||
| 
 | ||
|     return decorator
 | ||
| 
 | ||
| 
 | ||
| def call(command, *, quiet, **kwargs):
 | ||
|     """Execute a command.
 | ||
| 
 | ||
|     If 'quiet' is true, then redirect stdout and stderr to a temporary file.
 | ||
|     """
 | ||
|     print("โฏ", " ".join(map(str, command)))
 | ||
|     if not quiet:
 | ||
|         stdout = None
 | ||
|         stderr = None
 | ||
|     else:
 | ||
|         stdout = tempfile.NamedTemporaryFile(
 | ||
|             "w",
 | ||
|             encoding="utf-8",
 | ||
|             delete=False,
 | ||
|             prefix="cpython-emscripten-",
 | ||
|             suffix=".log",
 | ||
|         )
 | ||
|         stderr = subprocess.STDOUT
 | ||
|         print(f"๐ Logging output to {stdout.name} (--quiet)...")
 | ||
| 
 | ||
|     subprocess.check_call(command, **kwargs, stdout=stdout, stderr=stderr)
 | ||
| 
 | ||
| 
 | ||
| def build_platform():
 | ||
|     """The name of the build/host platform."""
 | ||
|     # Can also be found via `config.guess`.`
 | ||
|     return sysconfig.get_config_var("BUILD_GNU_TYPE")
 | ||
| 
 | ||
| 
 | ||
| def build_python_path():
 | ||
|     """The path to the build Python binary."""
 | ||
|     binary = NATIVE_BUILD_DIR / "python"
 | ||
|     if not binary.is_file():
 | ||
|         binary = binary.with_suffix(".exe")
 | ||
|         if not binary.is_file():
 | ||
|             raise FileNotFoundError("Unable to find `python(.exe)` in " f"{NATIVE_BUILD_DIR}")
 | ||
| 
 | ||
|     return binary
 | ||
| 
 | ||
| 
 | ||
| @subdir(NATIVE_BUILD_DIR, clean_ok=True)
 | ||
| def configure_build_python(context, working_dir):
 | ||
|     """Configure the build/host Python."""
 | ||
|     if LOCAL_SETUP.exists():
 | ||
|         print(f"๐ {LOCAL_SETUP} exists ...")
 | ||
|     else:
 | ||
|         print(f"๐ Touching {LOCAL_SETUP} ...")
 | ||
|         LOCAL_SETUP.write_bytes(LOCAL_SETUP_MARKER)
 | ||
| 
 | ||
|     configure = [os.path.relpath(CHECKOUT / "configure", working_dir)]
 | ||
|     if context.args:
 | ||
|         configure.extend(context.args)
 | ||
| 
 | ||
|     call(configure, quiet=context.quiet)
 | ||
| 
 | ||
| 
 | ||
| @subdir(NATIVE_BUILD_DIR)
 | ||
| def make_build_python(context, working_dir):
 | ||
|     """Make/build the build Python."""
 | ||
|     call(["make", "--jobs", str(cpu_count()), "all"], quiet=context.quiet)
 | ||
| 
 | ||
|     binary = build_python_path()
 | ||
|     cmd = [
 | ||
|         binary,
 | ||
|         "-c",
 | ||
|         "import sys; " "print(f'{sys.version_info.major}.{sys.version_info.minor}')",
 | ||
|     ]
 | ||
|     version = subprocess.check_output(cmd, encoding="utf-8").strip()
 | ||
| 
 | ||
|     print(f"๐ {binary} {version}")
 | ||
| 
 | ||
| 
 | ||
| def check_shasum(file: str, expected_shasum: str):
 | ||
|     with open(file, "rb") as f:
 | ||
|         digest = hashlib.file_digest(f, "sha256")
 | ||
|     if digest.hexdigest() != expected_shasum:
 | ||
|         raise RuntimeError(f"Unexpected shasum for {file}")
 | ||
| 
 | ||
| 
 | ||
| def download_and_unpack(working_dir: Path, url: str, expected_shasum: str):
 | ||
|     with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete_on_close=False) as tmp_file:
 | ||
|         with urlopen(url) as response:
 | ||
|             shutil.copyfileobj(response, tmp_file)
 | ||
|         tmp_file.close()
 | ||
|         check_shasum(tmp_file.name, expected_shasum)
 | ||
|         shutil.unpack_archive(tmp_file.name, working_dir)
 | ||
| 
 | ||
| 
 | ||
| @subdir(HOST_BUILD_DIR, clean_ok=True)
 | ||
| def make_emscripten_libffi(context, working_dir):
 | ||
|     ver = "3.4.6"
 | ||
|     libffi_dir = working_dir / f"libffi-{ver}"
 | ||
|     shutil.rmtree(libffi_dir, ignore_errors=True)
 | ||
|     download_and_unpack(working_dir, f"https://github.com/libffi/libffi/releases/download/v{ver}/libffi-{ver}.tar.gz", "b0dea9df23c863a7a50e825440f3ebffabd65df1497108e5d437747843895a4e")
 | ||
|     call(
 | ||
|         [EMSCRIPTEN_DIR / "make_libffi.sh"],
 | ||
|         env=updated_env({"PREFIX": PREFIX_DIR}),
 | ||
|         cwd=libffi_dir,
 | ||
|         quiet=context.quiet,
 | ||
|     )
 | ||
| 
 | ||
| 
 | ||
| @subdir(HOST_BUILD_DIR, clean_ok=True)
 | ||
| def make_mpdec(context, working_dir):
 | ||
|     ver = "4.0.1"
 | ||
|     mpdec_dir = working_dir / f"mpdecimal-{ver}"
 | ||
|     shutil.rmtree(mpdec_dir, ignore_errors=True)
 | ||
|     download_and_unpack(working_dir, f"https://www.bytereef.org/software/mpdecimal/releases/mpdecimal-{ver}.tar.gz", "96d33abb4bb0070c7be0fed4246cd38416188325f820468214471938545b1ac8")
 | ||
|     call(
 | ||
|         [
 | ||
|             "emconfigure",
 | ||
|             mpdec_dir / "configure",
 | ||
|             "CFLAGS=-fPIC",
 | ||
|             "--prefix",
 | ||
|             PREFIX_DIR,
 | ||
|             "--disable-shared",
 | ||
|         ],
 | ||
|         cwd=mpdec_dir,
 | ||
|         quiet=context.quiet,
 | ||
|     )
 | ||
|     call(
 | ||
|         [
 | ||
|             "make",
 | ||
|             "install"
 | ||
|         ],
 | ||
|         cwd=mpdec_dir,
 | ||
|         quiet=context.quiet,
 | ||
|     )
 | ||
| 
 | ||
| 
 | ||
| @subdir(HOST_DIR, clean_ok=True)
 | ||
| def configure_emscripten_python(context, working_dir):
 | ||
|     """Configure the emscripten/host build."""
 | ||
|     config_site = os.fsdecode(
 | ||
|         EMSCRIPTEN_DIR / "config.site-wasm32-emscripten"
 | ||
|     )
 | ||
| 
 | ||
|     emscripten_build_dir = working_dir.relative_to(CHECKOUT)
 | ||
| 
 | ||
|     python_build_dir = NATIVE_BUILD_DIR / "build"
 | ||
|     lib_dirs = list(python_build_dir.glob("lib.*"))
 | ||
|     assert (
 | ||
|         len(lib_dirs) == 1
 | ||
|     ), f"Expected a single lib.* directory in {python_build_dir}"
 | ||
|     lib_dir = os.fsdecode(lib_dirs[0])
 | ||
|     pydebug = lib_dir.endswith("-pydebug")
 | ||
|     python_version = lib_dir.removesuffix("-pydebug").rpartition("-")[-1]
 | ||
|     sysconfig_data = (
 | ||
|         f"{emscripten_build_dir}/build/lib.emscripten-wasm32-{python_version}"
 | ||
|     )
 | ||
|     if pydebug:
 | ||
|         sysconfig_data += "-pydebug"
 | ||
| 
 | ||
|     host_runner = context.host_runner
 | ||
|     if node_version := os.environ.get("PYTHON_NODE_VERSION", None):
 | ||
|         res = subprocess.run(
 | ||
|             [
 | ||
|                 "bash",
 | ||
|                 "-c",
 | ||
|                 f"source ~/.nvm/nvm.sh && nvm which {node_version}",
 | ||
|             ],
 | ||
|             text=True,
 | ||
|             capture_output=True,
 | ||
|         )
 | ||
|         host_runner = res.stdout.strip()
 | ||
|     pkg_config_path_dir = (PREFIX_DIR / "lib/pkgconfig/").resolve()
 | ||
|     env_additions = {
 | ||
|         "CONFIG_SITE": config_site,
 | ||
|         "HOSTRUNNER": host_runner,
 | ||
|         "EM_PKG_CONFIG_PATH": str(pkg_config_path_dir),
 | ||
|     }
 | ||
|     build_python = os.fsdecode(build_python_path())
 | ||
|     configure = [
 | ||
|         "emconfigure",
 | ||
|         os.path.relpath(CHECKOUT / "configure", working_dir),
 | ||
|         "CFLAGS=-DPY_CALL_TRAMPOLINE -sUSE_BZIP2",
 | ||
|         "PKG_CONFIG=pkg-config",
 | ||
|         f"--host={HOST_TRIPLE}",
 | ||
|         f"--build={build_platform()}",
 | ||
|         f"--with-build-python={build_python}",
 | ||
|         "--without-pymalloc",
 | ||
|         "--disable-shared",
 | ||
|         "--disable-ipv6",
 | ||
|         "--enable-big-digits=30",
 | ||
|         "--enable-wasm-dynamic-linking",
 | ||
|         f"--prefix={PREFIX_DIR}",
 | ||
|     ]
 | ||
|     if pydebug:
 | ||
|         configure.append("--with-pydebug")
 | ||
|     if context.args:
 | ||
|         configure.extend(context.args)
 | ||
|     call(
 | ||
|         configure,
 | ||
|         env=updated_env(env_additions),
 | ||
|         quiet=context.quiet,
 | ||
|     )
 | ||
| 
 | ||
|     shutil.copy(EMSCRIPTEN_DIR / "node_entry.mjs", working_dir / "node_entry.mjs")
 | ||
| 
 | ||
|     node_entry = working_dir / "node_entry.mjs"
 | ||
|     exec_script = working_dir / "python.sh"
 | ||
|     exec_script.write_text(
 | ||
|         dedent(
 | ||
|             f"""\
 | ||
|             #!/bin/sh
 | ||
| 
 | ||
|             # Macs come with FreeBSD coreutils which doesn't have the -s option
 | ||
|             # so feature detect and work around it.
 | ||
|             if which grealpath > /dev/null 2>&1; then
 | ||
|                 # It has brew installed gnu core utils, use that
 | ||
|                 REALPATH="grealpath -s"
 | ||
|             elif which realpath > /dev/null 2>&1 && realpath --version > /dev/null 2>&1 && realpath --version | grep GNU > /dev/null 2>&1; then
 | ||
|                 # realpath points to GNU realpath so use it.
 | ||
|                 REALPATH="realpath -s"
 | ||
|             else
 | ||
|                 # Shim for macs without GNU coreutils
 | ||
|                 abs_path () {{
 | ||
|                     echo "$(cd "$(dirname "$1")" || exit; pwd)/$(basename "$1")"
 | ||
|                 }}
 | ||
|                 REALPATH=abs_path
 | ||
|             fi
 | ||
| 
 | ||
|             # Before node 24, --experimental-wasm-jspi uses different API,
 | ||
|             # After node 24 JSPI is on by default.
 | ||
|             ARGS=$({host_runner} -e "$(cat <<"EOF"
 | ||
|             const major_version = Number(process.version.split(".")[0].slice(1));
 | ||
|             if (major_version === 24) {{
 | ||
|                 process.stdout.write("--experimental-wasm-jspi");
 | ||
|             }}
 | ||
|             EOF
 | ||
|             )")
 | ||
| 
 | ||
|             # We compute our own path, not following symlinks and pass it in so that
 | ||
|             # node_entry.mjs can set sys.executable correctly.
 | ||
|             # Intentionally allow word splitting on NODEFLAGS.
 | ||
|             exec {host_runner} $NODEFLAGS $ARGS {node_entry} --this-program="$($REALPATH "$0")" "$@"
 | ||
|             """
 | ||
|         )
 | ||
|     )
 | ||
|     exec_script.chmod(0o755)
 | ||
|     print(f"๐โโ๏ธ Created {exec_script} ... ")
 | ||
|     sys.stdout.flush()
 | ||
| 
 | ||
| 
 | ||
| @subdir(HOST_DIR)
 | ||
| def make_emscripten_python(context, working_dir):
 | ||
|     """Run `make` for the emscripten/host build."""
 | ||
|     call(
 | ||
|         ["make", "--jobs", str(cpu_count()), "all"],
 | ||
|         env=updated_env(),
 | ||
|         quiet=context.quiet,
 | ||
|     )
 | ||
| 
 | ||
|     exec_script = working_dir / "python.sh"
 | ||
|     subprocess.check_call([exec_script, "--version"])
 | ||
| 
 | ||
| 
 | ||
| def build_all(context):
 | ||
|     """Build everything."""
 | ||
|     steps = [
 | ||
|         configure_build_python,
 | ||
|         make_build_python,
 | ||
|         make_emscripten_libffi,
 | ||
|         make_mpdec,
 | ||
|         configure_emscripten_python,
 | ||
|         make_emscripten_python,
 | ||
|     ]
 | ||
|     for step in steps:
 | ||
|         step(context)
 | ||
| 
 | ||
| 
 | ||
| def clean_contents(context):
 | ||
|     """Delete all files created by this script."""
 | ||
|     if CROSS_BUILD_DIR.exists():
 | ||
|         print(f"๐งน Deleting {CROSS_BUILD_DIR} ...")
 | ||
|         shutil.rmtree(CROSS_BUILD_DIR)
 | ||
| 
 | ||
|     if LOCAL_SETUP.exists():
 | ||
|         with LOCAL_SETUP.open("rb") as file:
 | ||
|             if file.read(len(LOCAL_SETUP_MARKER)) == LOCAL_SETUP_MARKER:
 | ||
|                 print(f"๐งน Deleting generated {LOCAL_SETUP} ...")
 | ||
| 
 | ||
| 
 | ||
| def main():
 | ||
|     default_host_runner = "node"
 | ||
| 
 | ||
|     parser = argparse.ArgumentParser()
 | ||
|     subcommands = parser.add_subparsers(dest="subcommand")
 | ||
|     build = subcommands.add_parser("build", help="Build everything")
 | ||
|     configure_build = subcommands.add_parser(
 | ||
|         "configure-build-python", help="Run `configure` for the " "build Python"
 | ||
|     )
 | ||
|     make_mpdec_cmd = subcommands.add_parser(
 | ||
|         "make-mpdec", help="Clone mpdec repo, configure and build it for emscripten"
 | ||
|     )
 | ||
|     make_libffi_cmd = subcommands.add_parser(
 | ||
|         "make-libffi", help="Clone libffi repo, configure and build it for emscripten"
 | ||
|     )
 | ||
|     make_build = subcommands.add_parser(
 | ||
|         "make-build-python", help="Run `make` for the build Python"
 | ||
|     )
 | ||
|     configure_host = subcommands.add_parser(
 | ||
|         "configure-host",
 | ||
|         help="Run `configure` for the host/emscripten (pydebug builds are inferred from the build Python)",
 | ||
|     )
 | ||
|     make_host = subcommands.add_parser(
 | ||
|         "make-host", help="Run `make` for the host/emscripten"
 | ||
|     )
 | ||
|     clean = subcommands.add_parser(
 | ||
|         "clean", help="Delete files and directories created by this script"
 | ||
|     )
 | ||
|     for subcommand in (
 | ||
|         build,
 | ||
|         configure_build,
 | ||
|         make_libffi_cmd,
 | ||
|         make_mpdec_cmd,
 | ||
|         make_build,
 | ||
|         configure_host,
 | ||
|         make_host,
 | ||
|     ):
 | ||
|         subcommand.add_argument(
 | ||
|             "--quiet",
 | ||
|             action="store_true",
 | ||
|             default=False,
 | ||
|             dest="quiet",
 | ||
|             help="Redirect output from subprocesses to a log file",
 | ||
|         )
 | ||
|     for subcommand in configure_build, configure_host:
 | ||
|         subcommand.add_argument(
 | ||
|             "--clean",
 | ||
|             action="store_true",
 | ||
|             default=False,
 | ||
|             dest="clean",
 | ||
|             help="Delete any relevant directories before building",
 | ||
|         )
 | ||
|     for subcommand in build, configure_build, configure_host:
 | ||
|         subcommand.add_argument(
 | ||
|             "args", nargs="*", help="Extra arguments to pass to `configure`"
 | ||
|         )
 | ||
|     for subcommand in build, configure_host:
 | ||
|         subcommand.add_argument(
 | ||
|             "--host-runner",
 | ||
|             action="store",
 | ||
|             default=default_host_runner,
 | ||
|             dest="host_runner",
 | ||
|             help="Command template for running the emscripten host"
 | ||
|             f"`{default_host_runner}`)",
 | ||
|         )
 | ||
| 
 | ||
|     context = parser.parse_args()
 | ||
| 
 | ||
|     dispatch = {
 | ||
|         "make-libffi": make_emscripten_libffi,
 | ||
|         "make-mpdec": make_mpdec,
 | ||
|         "configure-build-python": configure_build_python,
 | ||
|         "make-build-python": make_build_python,
 | ||
|         "configure-host": configure_emscripten_python,
 | ||
|         "make-host": make_emscripten_python,
 | ||
|         "build": build_all,
 | ||
|         "clean": clean_contents,
 | ||
|     }
 | ||
| 
 | ||
|     if not context.subcommand:
 | ||
|         # No command provided, display help and exit
 | ||
|         print("Expected one of", ", ".join(sorted(dispatch.keys())), file=sys.stderr)
 | ||
|         parser.print_help(sys.stderr)
 | ||
|         sys.exit(1)
 | ||
|     dispatch[context.subcommand](context)
 | ||
| 
 | ||
| 
 | ||
| if __name__ == "__main__":
 | ||
|     main()
 | 
