mirror of
https://github.com/python/cpython.git
synced 2026-04-15 08:11:10 +00:00
863 lines
26 KiB
Python
863 lines
26 KiB
Python
#!/usr/bin/env python3
|
||
|
||
import argparse
|
||
import contextlib
|
||
import functools
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import sysconfig
|
||
import tempfile
|
||
from pathlib import Path
|
||
from textwrap import dedent
|
||
from urllib.request import urlopen
|
||
|
||
import tomllib
|
||
|
||
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
|
||
CONFIG_FILE = EMSCRIPTEN_DIR / "config.toml"
|
||
|
||
DEFAULT_CROSS_BUILD_DIR = CHECKOUT / "cross-build"
|
||
HOST_TRIPLE = "wasm32-emscripten"
|
||
|
||
|
||
@functools.cache
|
||
def load_config_toml():
|
||
with CONFIG_FILE.open("rb") as file:
|
||
return tomllib.load(file)
|
||
|
||
|
||
@functools.cache
|
||
def required_emscripten_version():
|
||
return load_config_toml()["emscripten-version"]
|
||
|
||
|
||
@functools.cache
|
||
def emsdk_cache_root(emsdk_cache):
|
||
required_version = required_emscripten_version()
|
||
return Path(emsdk_cache).absolute() / required_version
|
||
|
||
|
||
@functools.cache
|
||
def emsdk_activate_path(emsdk_cache):
|
||
return emsdk_cache_root(emsdk_cache) / "emsdk/emsdk_env.sh"
|
||
|
||
|
||
def get_build_paths(cross_build_dir=None, emsdk_cache=None):
|
||
"""Compute all build paths from the given cross-build directory."""
|
||
if cross_build_dir is None:
|
||
cross_build_dir = DEFAULT_CROSS_BUILD_DIR
|
||
cross_build_dir = Path(cross_build_dir).absolute()
|
||
host_triple_dir = cross_build_dir / HOST_TRIPLE
|
||
prefix_dir = host_triple_dir / "prefix"
|
||
if emsdk_cache:
|
||
prefix_dir = emsdk_cache_root(emsdk_cache) / "prefix"
|
||
|
||
return {
|
||
"cross_build_dir": cross_build_dir,
|
||
"native_build_dir": cross_build_dir / "build",
|
||
"host_triple_dir": host_triple_dir,
|
||
"host_build_dir": host_triple_dir / "build",
|
||
"host_dir": host_triple_dir / "build" / "python",
|
||
"prefix_dir": prefix_dir,
|
||
}
|
||
|
||
|
||
LOCAL_SETUP = CHECKOUT / "Modules" / "Setup.local"
|
||
LOCAL_SETUP_MARKER = b"# Generated by Platforms/wasm/emscripten.py\n"
|
||
|
||
|
||
@functools.cache
|
||
def validate_emsdk_version(emsdk_cache):
|
||
"""Validate that the emsdk cache contains the required emscripten version."""
|
||
if emsdk_cache is None:
|
||
print("Build will use EMSDK from current environment.")
|
||
return
|
||
required_version = required_emscripten_version()
|
||
emsdk_env = emsdk_activate_path(emsdk_cache)
|
||
if not emsdk_env.is_file():
|
||
print(
|
||
f"Required emscripten version {required_version} not found in {emsdk_cache}",
|
||
file=sys.stderr,
|
||
)
|
||
sys.exit(1)
|
||
print(f"✅ Emscripten version {required_version} found in {emsdk_cache}")
|
||
|
||
|
||
def parse_env(text):
|
||
result = {}
|
||
for line in text.splitlines():
|
||
key, val = line.split("=", 1)
|
||
result[key] = val
|
||
return result
|
||
|
||
|
||
@functools.cache
|
||
def get_emsdk_environ(emsdk_cache):
|
||
"""Returns os.environ updated by sourcing emsdk_env.sh"""
|
||
if not emsdk_cache:
|
||
return os.environ
|
||
env_text = subprocess.check_output(
|
||
[
|
||
"bash",
|
||
"-c",
|
||
f"EMSDK_QUIET=1 source {emsdk_activate_path(emsdk_cache)} && env",
|
||
],
|
||
text=True,
|
||
)
|
||
return parse_env(env_text)
|
||
|
||
|
||
def updated_env(updates, emsdk_cache):
|
||
"""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 | get_emsdk_environ(emsdk_cache) | 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(path_key, *, clean_ok=False):
|
||
"""Decorator to change to a working directory.
|
||
|
||
path_key is a key into context.build_paths, used to resolve the working
|
||
directory at call time.
|
||
"""
|
||
|
||
def decorator(func):
|
||
@functools.wraps(func)
|
||
def wrapper(context):
|
||
working_dir = context.build_paths[path_key]
|
||
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(context):
|
||
"""The path to the build Python binary."""
|
||
native_build_dir = context.build_paths["native_build_dir"]
|
||
binary = native_build_dir / "python"
|
||
if not binary.is_file():
|
||
binary = binary.with_suffix(".exe")
|
||
if not binary.is_file():
|
||
raise FileNotFoundError(
|
||
f"Unable to find `python(.exe)` in {native_build_dir}"
|
||
)
|
||
|
||
return binary
|
||
|
||
|
||
def install_emscripten(context):
|
||
emsdk_cache = context.emsdk_cache
|
||
if emsdk_cache is None:
|
||
print("install-emscripten requires --emsdk-cache", file=sys.stderr)
|
||
sys.exit(1)
|
||
version = required_emscripten_version()
|
||
emsdk_target = emsdk_cache_root(emsdk_cache) / "emsdk"
|
||
if emsdk_target.exists():
|
||
if not context.quiet:
|
||
print(f"Emscripten version {version} already installed")
|
||
return
|
||
if not context.quiet:
|
||
print(f"Installing emscripten version {version}")
|
||
emsdk_target.mkdir(parents=True)
|
||
call(
|
||
[
|
||
"git",
|
||
"clone",
|
||
"https://github.com/emscripten-core/emsdk.git",
|
||
emsdk_target,
|
||
],
|
||
quiet=context.quiet,
|
||
)
|
||
call([emsdk_target / "emsdk", "install", version], quiet=context.quiet)
|
||
call([emsdk_target / "emsdk", "activate", version], quiet=context.quiet)
|
||
if not context.quiet:
|
||
print(f"Installed emscripten version {version}")
|
||
|
||
|
||
@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(context)
|
||
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)
|
||
|
||
|
||
def should_build_library(prefix, name, config, quiet):
|
||
cached_config = prefix / (name + ".json")
|
||
if not cached_config.exists():
|
||
if not quiet:
|
||
print(
|
||
f"No cached build of {name} version {config['version']} found, building"
|
||
)
|
||
return True
|
||
|
||
try:
|
||
with cached_config.open("rb") as f:
|
||
cached_config = json.load(f)
|
||
except json.JSONDecodeError:
|
||
if not quiet:
|
||
print(f"Cached data for {name} invalid, rebuilding")
|
||
return True
|
||
if config == cached_config:
|
||
if not quiet:
|
||
print(
|
||
f"Found cached build of {name} version {config['version']}, not rebuilding"
|
||
)
|
||
return False
|
||
|
||
if not quiet:
|
||
print(
|
||
f"Found cached build of {name} version {config['version']} but it's out of date, rebuilding"
|
||
)
|
||
return True
|
||
|
||
|
||
def write_library_config(prefix, name, config, quiet):
|
||
cached_config = prefix / (name + ".json")
|
||
with cached_config.open("w") as f:
|
||
json.dump(config, f)
|
||
if not quiet:
|
||
print(f"Succeded building {name}, wrote config to {cached_config}")
|
||
|
||
|
||
@subdir("host_build_dir", clean_ok=True)
|
||
def make_emscripten_libffi(context, working_dir):
|
||
validate_emsdk_version(context.emsdk_cache)
|
||
prefix = context.build_paths["prefix_dir"]
|
||
libffi_config = load_config_toml()["dependencies"]["libffi"]
|
||
with open(EMSCRIPTEN_DIR / "make_libffi.sh", "rb") as f:
|
||
libffi_config["make_libffi_shasum"] = hashlib.file_digest(f, "sha256").hexdigest()
|
||
if not should_build_library(
|
||
prefix, "libffi", libffi_config, context.quiet
|
||
):
|
||
return
|
||
|
||
if context.check_up_to_date:
|
||
print("libffi out of date, expected to be up to date", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
url = libffi_config["url"]
|
||
version = libffi_config["version"]
|
||
shasum = libffi_config["shasum"]
|
||
libffi_dir = working_dir / f"libffi-{version}"
|
||
shutil.rmtree(libffi_dir, ignore_errors=True)
|
||
download_and_unpack(
|
||
working_dir,
|
||
url.format(version=version),
|
||
shasum,
|
||
)
|
||
call(
|
||
[EMSCRIPTEN_DIR / "make_libffi.sh"],
|
||
env=updated_env({"PREFIX": prefix}, context.emsdk_cache),
|
||
cwd=libffi_dir,
|
||
quiet=context.quiet,
|
||
)
|
||
write_library_config(prefix, "libffi", libffi_config, context.quiet)
|
||
|
||
|
||
@subdir("host_build_dir", clean_ok=True)
|
||
def make_mpdec(context, working_dir):
|
||
validate_emsdk_version(context.emsdk_cache)
|
||
prefix = context.build_paths["prefix_dir"]
|
||
mpdec_config = load_config_toml()["dependencies"]["mpdec"]
|
||
if not should_build_library(prefix, "mpdec", mpdec_config, context.quiet):
|
||
return
|
||
|
||
if context.check_up_to_date:
|
||
print("libmpdec out of date, expected to be up to date", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
url = mpdec_config["url"]
|
||
version = mpdec_config["version"]
|
||
shasum = mpdec_config["shasum"]
|
||
mpdec_dir = working_dir / f"mpdecimal-{version}"
|
||
shutil.rmtree(mpdec_dir, ignore_errors=True)
|
||
download_and_unpack(
|
||
working_dir,
|
||
url.format(version=version),
|
||
shasum,
|
||
)
|
||
call(
|
||
[
|
||
"emconfigure",
|
||
mpdec_dir / "configure",
|
||
"CFLAGS=-fPIC",
|
||
"--prefix",
|
||
prefix,
|
||
"--disable-shared",
|
||
],
|
||
cwd=mpdec_dir,
|
||
quiet=context.quiet,
|
||
env=updated_env({}, context.emsdk_cache),
|
||
)
|
||
call(
|
||
["make", "install"],
|
||
cwd=mpdec_dir,
|
||
quiet=context.quiet,
|
||
)
|
||
write_library_config(prefix, "mpdec", mpdec_config, context.quiet)
|
||
|
||
|
||
def make_dependencies(context):
|
||
make_emscripten_libffi(context)
|
||
make_mpdec(context)
|
||
|
||
|
||
def calculate_node_path():
|
||
node_version = os.environ.get("PYTHON_NODE_VERSION", None)
|
||
if node_version is None:
|
||
node_version = load_config_toml()["node-version"]
|
||
|
||
subprocess.run(
|
||
[
|
||
"bash",
|
||
"-c",
|
||
f"source ~/.nvm/nvm.sh && nvm install {node_version}",
|
||
],
|
||
check=True,
|
||
)
|
||
|
||
res = subprocess.run(
|
||
[
|
||
"bash",
|
||
"-c",
|
||
f"source ~/.nvm/nvm.sh && nvm which {node_version}",
|
||
],
|
||
text=True,
|
||
capture_output=True,
|
||
check=True,
|
||
)
|
||
return res.stdout.strip()
|
||
|
||
|
||
@subdir("host_dir", clean_ok=True)
|
||
def configure_emscripten_python(context, working_dir):
|
||
"""Configure the emscripten/host build."""
|
||
validate_emsdk_version(context.emsdk_cache)
|
||
host_runner = context.host_runner
|
||
if host_runner is None:
|
||
host_runner = calculate_node_path()
|
||
|
||
paths = context.build_paths
|
||
config_site = os.fsdecode(EMSCRIPTEN_DIR / "config.site-wasm32-emscripten")
|
||
|
||
emscripten_build_dir = working_dir.relative_to(CHECKOUT)
|
||
|
||
python_build_dir = paths["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"
|
||
pkg_config_path_dir = (paths["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(context))
|
||
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={paths['prefix_dir']}",
|
||
]
|
||
if pydebug:
|
||
configure.append("--with-pydebug")
|
||
if context.args:
|
||
configure.extend(context.args)
|
||
call(
|
||
configure,
|
||
env=updated_env(env_additions, context.emsdk_cache),
|
||
quiet=context.quiet,
|
||
)
|
||
|
||
shutil.copy(
|
||
EMSCRIPTEN_DIR / "node_entry.mjs", working_dir / "node_entry.mjs"
|
||
)
|
||
|
||
shutil.copy(
|
||
EMSCRIPTEN_DIR / "streams.mjs", working_dir / "streams.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({}, context.emsdk_cache),
|
||
quiet=context.quiet,
|
||
)
|
||
|
||
exec_script = working_dir / "python.sh"
|
||
subprocess.check_call([exec_script, "--version"])
|
||
|
||
|
||
def run_emscripten_python(context):
|
||
"""Run the built emscripten Python."""
|
||
host_dir = context.build_paths["host_dir"]
|
||
exec_script = host_dir / "python.sh"
|
||
if not exec_script.is_file():
|
||
print("Emscripten not built", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
args = context.args
|
||
# Strip the "--" separator if present
|
||
if args and args[0] == "--":
|
||
args = args[1:]
|
||
|
||
if context.test:
|
||
args = load_config_toml()["test-args"] + args
|
||
elif context.pythoninfo:
|
||
args = load_config_toml()["pythoninfo-args"] + args
|
||
|
||
os.execv(str(exec_script), [str(exec_script), *args])
|
||
|
||
|
||
def build_target(context):
|
||
"""Build one or more targets."""
|
||
steps = []
|
||
if context.target in {"build", "all"}:
|
||
steps.extend([
|
||
configure_build_python,
|
||
make_build_python,
|
||
])
|
||
if context.target in {"host", "all"}:
|
||
steps.extend([
|
||
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 context.target in {"all", "build"}:
|
||
build_dir = context.build_paths["native_build_dir"]
|
||
if build_dir.exists():
|
||
print(f"🧹 Deleting {build_dir} ...")
|
||
shutil.rmtree(build_dir)
|
||
|
||
if context.target in {"all", "host"}:
|
||
host_triple_dir = context.build_paths["host_triple_dir"]
|
||
if host_triple_dir.exists():
|
||
print(f"🧹 Deleting {host_triple_dir} ...")
|
||
shutil.rmtree(host_triple_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 add_cross_build_dir_option(subcommand):
|
||
subcommand.add_argument(
|
||
"--cross-build-dir",
|
||
action="store",
|
||
default=os.environ.get("CROSS_BUILD_DIR"),
|
||
dest="cross_build_dir",
|
||
help=(
|
||
"Path to the cross-build directory "
|
||
f"(default: {DEFAULT_CROSS_BUILD_DIR}). "
|
||
"Can also be set with the CROSS_BUILD_DIR environment variable.",
|
||
),
|
||
)
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser()
|
||
subcommands = parser.add_subparsers(dest="subcommand")
|
||
|
||
install_emscripten_cmd = subcommands.add_parser(
|
||
"install-emscripten",
|
||
help="Install the appropriate version of Emscripten",
|
||
)
|
||
|
||
build = subcommands.add_parser("build", help="Build everything")
|
||
build.add_argument(
|
||
"target",
|
||
nargs="?",
|
||
default="all",
|
||
choices=["all", "host", "build"],
|
||
help=(
|
||
"What should be built. 'build' for just the build platform, or "
|
||
"'host' for the host platform, or 'all' for both. Defaults to 'all'."
|
||
),
|
||
)
|
||
|
||
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_dependencies_cmd = subcommands.add_parser(
|
||
"make-dependencies",
|
||
help="Build all static library dependencies",
|
||
)
|
||
|
||
for cmd in [make_mpdec_cmd, make_libffi_cmd, make_dependencies_cmd]:
|
||
cmd.add_argument(
|
||
"--check-up-to-date",
|
||
action="store_true",
|
||
default=False,
|
||
help=("If passed, will fail if dependency is out of date"),
|
||
)
|
||
|
||
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"
|
||
)
|
||
|
||
run = subcommands.add_parser(
|
||
"run",
|
||
help="Run the built emscripten Python",
|
||
)
|
||
run.add_argument(
|
||
"--test",
|
||
action="store_true",
|
||
default=False,
|
||
help=(
|
||
"Add the default test arguments to the beginning of the command. "
|
||
"Default arguments loaded from Platforms/emscripten/config.toml"
|
||
),
|
||
)
|
||
run.add_argument(
|
||
"--pythoninfo",
|
||
action="store_true",
|
||
default=False,
|
||
help="Run -m test.pythoninfo",
|
||
)
|
||
run.add_argument(
|
||
"args",
|
||
nargs=argparse.REMAINDER,
|
||
help=(
|
||
"Arguments to pass to the emscripten Python "
|
||
"(use '--' to separate from run options)",
|
||
),
|
||
)
|
||
add_cross_build_dir_option(run)
|
||
|
||
clean = subcommands.add_parser(
|
||
"clean", help="Delete files and directories created by this script"
|
||
)
|
||
clean.add_argument(
|
||
"target",
|
||
nargs="?",
|
||
default="host",
|
||
choices=["all", "host", "build"],
|
||
help=(
|
||
"What should be cleaned. 'build' for just the build platform, or "
|
||
"'host' for the host platform, or 'all' for both. Defaults to 'host'."
|
||
),
|
||
)
|
||
|
||
for subcommand in (
|
||
install_emscripten_cmd,
|
||
build,
|
||
configure_build,
|
||
make_libffi_cmd,
|
||
make_mpdec_cmd,
|
||
make_dependencies_cmd,
|
||
make_build,
|
||
configure_host,
|
||
make_host,
|
||
clean,
|
||
):
|
||
subcommand.add_argument(
|
||
"--quiet",
|
||
action="store_true",
|
||
default="QUIET" in os.environ,
|
||
dest="quiet",
|
||
help=(
|
||
"Redirect output from subprocesses to a log file. "
|
||
"Can also be set with the QUIET environment variable."
|
||
),
|
||
)
|
||
add_cross_build_dir_option(subcommand)
|
||
subcommand.add_argument(
|
||
"--emsdk-cache",
|
||
action="store",
|
||
default=os.environ.get("EMSDK_CACHE"),
|
||
dest="emsdk_cache",
|
||
help=(
|
||
"Path to emsdk cache directory. If provided, validates that "
|
||
"the required emscripten version is installed. "
|
||
"Can also be set with the EMSDK_CACHE environment variable."
|
||
),
|
||
)
|
||
|
||
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=None,
|
||
dest="host_runner",
|
||
help="Command template for running the emscripten host "
|
||
"(default: use nvm to install the node version specified in config.toml)",
|
||
)
|
||
|
||
context = parser.parse_args()
|
||
context.emsdk_cache = getattr(context, "emsdk_cache", None)
|
||
context.cross_build_dir = getattr(context, "cross_build_dir", None)
|
||
context.check_up_to_date = getattr(context, "check_up_to_date", False)
|
||
|
||
if context.emsdk_cache:
|
||
context.emsdk_cache = Path(context.emsdk_cache).absolute()
|
||
|
||
context.build_paths = get_build_paths(
|
||
context.cross_build_dir, context.emsdk_cache
|
||
)
|
||
|
||
dispatch = {
|
||
"install-emscripten": install_emscripten,
|
||
"make-libffi": make_emscripten_libffi,
|
||
"make-mpdec": make_mpdec,
|
||
"make-dependencies": make_dependencies,
|
||
"configure-build-python": configure_build_python,
|
||
"make-build-python": make_build_python,
|
||
"configure-host": configure_emscripten_python,
|
||
"make-host": make_emscripten_python,
|
||
"build": build_target,
|
||
"run": run_emscripten_python,
|
||
"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()
|