import contextlib
import functools
import hashlib
+import json
import os
import shutil
import subprocess
from textwrap import dedent
from urllib.request import urlopen
+import tomllib
+
try:
from os import process_cpu_count as cpu_count
except ImportError:
EMSCRIPTEN_DIR = Path(__file__).parent
CHECKOUT = EMSCRIPTEN_DIR.parent.parent.parent
-EMSCRIPTEN_VERSION_FILE = EMSCRIPTEN_DIR / "emscripten_version.txt"
+CONFIG_FILE = EMSCRIPTEN_DIR / "config.toml"
DEFAULT_CROSS_BUILD_DIR = CHECKOUT / "cross-build"
HOST_TRIPLE = "wasm32-emscripten"
-def get_build_paths(cross_build_dir=None):
+@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": host_triple_dir / "prefix",
+ "prefix_dir": prefix_dir,
}
LOCAL_SETUP_MARKER = b"# Generated by Tools/wasm/emscripten.py\n"
-@functools.cache
-def get_required_emscripten_version():
- """Read the required emscripten version from emscripten_version.txt."""
- return EMSCRIPTEN_VERSION_FILE.read_text().strip()
-
-
-@functools.cache
-def get_emsdk_activate_path(emsdk_cache):
- required_version = get_required_emscripten_version()
- return Path(emsdk_cache) / required_version / "emsdk_env.sh"
-
-
def validate_emsdk_version(emsdk_cache):
"""Validate that the emsdk cache contains the required emscripten version."""
- required_version = get_required_emscripten_version()
- emsdk_env = get_emsdk_activate_path(emsdk_cache)
+ 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}",
[
"bash",
"-c",
- f"EMSDK_QUIET=1 source {get_emsdk_activate_path(emsdk_cache)} && env",
+ f"EMSDK_QUIET=1 source {emsdk_activate_path(emsdk_cache)} && env",
],
text=True,
)
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."""
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):
- ver = "3.4.6"
- libffi_dir = working_dir / f"libffi-{ver}"
+ prefix = context.build_paths["prefix_dir"]
+ libffi_config = load_config_toml()["libffi"]
+ if not should_build_library(
+ prefix, "libffi", libffi_config, context.quiet
+ ):
+ return
+ 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,
- f"https://github.com/libffi/libffi/releases/download/v{ver}/libffi-{ver}.tar.gz",
- "b0dea9df23c863a7a50e825440f3ebffabd65df1497108e5d437747843895a4e",
+ url.format(version=version),
+ shasum,
)
call(
[EMSCRIPTEN_DIR / "make_libffi.sh"],
- env=updated_env(
- {"PREFIX": context.build_paths["prefix_dir"]}, context.emsdk_cache
- ),
+ 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):
- ver = "4.0.1"
- mpdec_dir = working_dir / f"mpdecimal-{ver}"
+ prefix = context.build_paths["prefix_dir"]
+ mpdec_config = load_config_toml()["mpdec"]
+ if not should_build_library(prefix, "mpdec", mpdec_config, context.quiet):
+ return
+
+ 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,
- f"https://www.bytereef.org/software/mpdecimal/releases/mpdecimal-{ver}.tar.gz",
- "96d33abb4bb0070c7be0fed4246cd38416188325f820468214471938545b1ac8",
+ url.format(version=version),
+ shasum,
)
call(
[
mpdec_dir / "configure",
"CFLAGS=-fPIC",
"--prefix",
- context.build_paths["prefix_dir"],
+ prefix,
"--disable-shared",
],
cwd=mpdec_dir,
cwd=mpdec_dir,
quiet=context.quiet,
)
+ write_library_config(prefix, "mpdec", mpdec_config, context.quiet)
@subdir("host_dir", clean_ok=True)
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,
- ]
+def build_target(context):
+ """Build one or more targets."""
+ steps = []
+ if context.target in {"all"}:
+ steps.append(install_emscripten)
+ 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)
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"
)
)
for subcommand in (
+ install_emscripten_cmd,
build,
configure_build,
make_libffi_cmd,
context = parser.parse_args()
- context.build_paths = get_build_paths(context.cross_build_dir)
-
- if context.emsdk_cache:
+ if context.emsdk_cache and context.subcommand != "install-emscripten":
validate_emsdk_version(context.emsdk_cache)
context.emsdk_cache = Path(context.emsdk_cache).absolute()
else:
print("Build will use EMSDK from current environment.")
+ 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,
"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,
+ "build": build_target,
"clean": clean_contents,
}