Skip to content

Commit ebb150e

Browse files
gh-145219: Cache Emscripten build dependencies, add install-emscripten (#145664)
Modifies the Emscripten build script to allow for caching of dependencies, and for automated installation of new EMSDK versions. Co-authored-by: Russell Keith-Magee <russell@keith-magee.com>
1 parent 5197ecb commit ebb150e

File tree

3 files changed

+184
-45
lines changed

3 files changed

+184
-45
lines changed

Tools/wasm/emscripten/__main__.py

Lines changed: 170 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import contextlib
55
import functools
66
import hashlib
7+
import json
78
import os
89
import shutil
910
import subprocess
@@ -14,6 +15,8 @@
1415
from textwrap import dedent
1516
from urllib.request import urlopen
1617

18+
import tomllib
19+
1720
try:
1821
from os import process_cpu_count as cpu_count
1922
except ImportError:
@@ -22,48 +25,62 @@
2225

2326
EMSCRIPTEN_DIR = Path(__file__).parent
2427
CHECKOUT = EMSCRIPTEN_DIR.parent.parent.parent
25-
EMSCRIPTEN_VERSION_FILE = EMSCRIPTEN_DIR / "emscripten_version.txt"
28+
CONFIG_FILE = EMSCRIPTEN_DIR / "config.toml"
2629

2730
DEFAULT_CROSS_BUILD_DIR = CHECKOUT / "cross-build"
2831
HOST_TRIPLE = "wasm32-emscripten"
2932

3033

31-
def get_build_paths(cross_build_dir=None):
34+
@functools.cache
35+
def load_config_toml():
36+
with CONFIG_FILE.open("rb") as file:
37+
return tomllib.load(file)
38+
39+
40+
@functools.cache
41+
def required_emscripten_version():
42+
return load_config_toml()["emscripten-version"]
43+
44+
45+
@functools.cache
46+
def emsdk_cache_root(emsdk_cache):
47+
required_version = required_emscripten_version()
48+
return Path(emsdk_cache).absolute() / required_version
49+
50+
51+
@functools.cache
52+
def emsdk_activate_path(emsdk_cache):
53+
return emsdk_cache_root(emsdk_cache) / "emsdk/emsdk_env.sh"
54+
55+
56+
def get_build_paths(cross_build_dir=None, emsdk_cache=None):
3257
"""Compute all build paths from the given cross-build directory."""
3358
if cross_build_dir is None:
3459
cross_build_dir = DEFAULT_CROSS_BUILD_DIR
3560
cross_build_dir = Path(cross_build_dir).absolute()
3661
host_triple_dir = cross_build_dir / HOST_TRIPLE
62+
prefix_dir = host_triple_dir / "prefix"
63+
if emsdk_cache:
64+
prefix_dir = emsdk_cache_root(emsdk_cache) / "prefix"
65+
3766
return {
3867
"cross_build_dir": cross_build_dir,
3968
"native_build_dir": cross_build_dir / "build",
4069
"host_triple_dir": host_triple_dir,
4170
"host_build_dir": host_triple_dir / "build",
4271
"host_dir": host_triple_dir / "build" / "python",
43-
"prefix_dir": host_triple_dir / "prefix",
72+
"prefix_dir": prefix_dir,
4473
}
4574

4675

4776
LOCAL_SETUP = CHECKOUT / "Modules" / "Setup.local"
4877
LOCAL_SETUP_MARKER = b"# Generated by Tools/wasm/emscripten.py\n"
4978

5079

51-
@functools.cache
52-
def get_required_emscripten_version():
53-
"""Read the required emscripten version from emscripten_version.txt."""
54-
return EMSCRIPTEN_VERSION_FILE.read_text().strip()
55-
56-
57-
@functools.cache
58-
def get_emsdk_activate_path(emsdk_cache):
59-
required_version = get_required_emscripten_version()
60-
return Path(emsdk_cache) / required_version / "emsdk_env.sh"
61-
62-
6380
def validate_emsdk_version(emsdk_cache):
6481
"""Validate that the emsdk cache contains the required emscripten version."""
65-
required_version = get_required_emscripten_version()
66-
emsdk_env = get_emsdk_activate_path(emsdk_cache)
82+
required_version = required_emscripten_version()
83+
emsdk_env = emsdk_activate_path(emsdk_cache)
6784
if not emsdk_env.is_file():
6885
print(
6986
f"Required emscripten version {required_version} not found in {emsdk_cache}",
@@ -90,7 +107,7 @@ def get_emsdk_environ(emsdk_cache):
90107
[
91108
"bash",
92109
"-c",
93-
f"EMSDK_QUIET=1 source {get_emsdk_activate_path(emsdk_cache)} && env",
110+
f"EMSDK_QUIET=1 source {emsdk_activate_path(emsdk_cache)} && env",
94111
],
95112
text=True,
96113
)
@@ -207,6 +224,35 @@ def build_python_path(context):
207224
return binary
208225

209226

227+
def install_emscripten(context):
228+
emsdk_cache = context.emsdk_cache
229+
if emsdk_cache is None:
230+
print("install-emscripten requires --emsdk-cache", file=sys.stderr)
231+
sys.exit(1)
232+
version = required_emscripten_version()
233+
emsdk_target = emsdk_cache_root(emsdk_cache) / "emsdk"
234+
if emsdk_target.exists():
235+
if not context.quiet:
236+
print(f"Emscripten version {version} already installed")
237+
return
238+
if not context.quiet:
239+
print(f"Installing emscripten version {version}")
240+
emsdk_target.mkdir(parents=True)
241+
call(
242+
[
243+
"git",
244+
"clone",
245+
"https://github.com/emscripten-core/emsdk.git",
246+
emsdk_target,
247+
],
248+
quiet=context.quiet,
249+
)
250+
call([emsdk_target / "emsdk", "install", version], quiet=context.quiet)
251+
call([emsdk_target / "emsdk", "activate", version], quiet=context.quiet)
252+
if not context.quiet:
253+
print(f"Installed emscripten version {version}")
254+
255+
210256
@subdir("native_build_dir", clean_ok=True)
211257
def configure_build_python(context, working_dir):
212258
"""Configure the build/host Python."""
@@ -258,43 +304,95 @@ def download_and_unpack(working_dir: Path, url: str, expected_shasum: str):
258304
shutil.unpack_archive(tmp_file.name, working_dir)
259305

260306

307+
def should_build_library(prefix, name, config, quiet):
308+
cached_config = prefix / (name + ".json")
309+
if not cached_config.exists():
310+
if not quiet:
311+
print(
312+
f"No cached build of {name} version {config['version']} found, building"
313+
)
314+
return True
315+
316+
try:
317+
with cached_config.open("rb") as f:
318+
cached_config = json.load(f)
319+
except json.JSONDecodeError:
320+
if not quiet:
321+
print(f"Cached data for {name} invalid, rebuilding")
322+
return True
323+
if config == cached_config:
324+
if not quiet:
325+
print(
326+
f"Found cached build of {name} version {config['version']}, not rebuilding"
327+
)
328+
return False
329+
330+
if not quiet:
331+
print(
332+
f"Found cached build of {name} version {config['version']} but it's out of date, rebuilding"
333+
)
334+
return True
335+
336+
337+
def write_library_config(prefix, name, config, quiet):
338+
cached_config = prefix / (name + ".json")
339+
with cached_config.open("w") as f:
340+
json.dump(config, f)
341+
if not quiet:
342+
print(f"Succeded building {name}, wrote config to {cached_config}")
343+
344+
261345
@subdir("host_build_dir", clean_ok=True)
262346
def make_emscripten_libffi(context, working_dir):
263-
ver = "3.4.6"
264-
libffi_dir = working_dir / f"libffi-{ver}"
347+
prefix = context.build_paths["prefix_dir"]
348+
libffi_config = load_config_toml()["libffi"]
349+
if not should_build_library(
350+
prefix, "libffi", libffi_config, context.quiet
351+
):
352+
return
353+
url = libffi_config["url"]
354+
version = libffi_config["version"]
355+
shasum = libffi_config["shasum"]
356+
libffi_dir = working_dir / f"libffi-{version}"
265357
shutil.rmtree(libffi_dir, ignore_errors=True)
266358
download_and_unpack(
267359
working_dir,
268-
f"https://github.com/libffi/libffi/releases/download/v{ver}/libffi-{ver}.tar.gz",
269-
"b0dea9df23c863a7a50e825440f3ebffabd65df1497108e5d437747843895a4e",
360+
url.format(version=version),
361+
shasum,
270362
)
271363
call(
272364
[EMSCRIPTEN_DIR / "make_libffi.sh"],
273-
env=updated_env(
274-
{"PREFIX": context.build_paths["prefix_dir"]}, context.emsdk_cache
275-
),
365+
env=updated_env({"PREFIX": prefix}, context.emsdk_cache),
276366
cwd=libffi_dir,
277367
quiet=context.quiet,
278368
)
369+
write_library_config(prefix, "libffi", libffi_config, context.quiet)
279370

280371

281372
@subdir("host_build_dir", clean_ok=True)
282373
def make_mpdec(context, working_dir):
283-
ver = "4.0.1"
284-
mpdec_dir = working_dir / f"mpdecimal-{ver}"
374+
prefix = context.build_paths["prefix_dir"]
375+
mpdec_config = load_config_toml()["mpdec"]
376+
if not should_build_library(prefix, "mpdec", mpdec_config, context.quiet):
377+
return
378+
379+
url = mpdec_config["url"]
380+
version = mpdec_config["version"]
381+
shasum = mpdec_config["shasum"]
382+
mpdec_dir = working_dir / f"mpdecimal-{version}"
285383
shutil.rmtree(mpdec_dir, ignore_errors=True)
286384
download_and_unpack(
287385
working_dir,
288-
f"https://www.bytereef.org/software/mpdecimal/releases/mpdecimal-{ver}.tar.gz",
289-
"96d33abb4bb0070c7be0fed4246cd38416188325f820468214471938545b1ac8",
386+
url.format(version=version),
387+
shasum,
290388
)
291389
call(
292390
[
293391
"emconfigure",
294392
mpdec_dir / "configure",
295393
"CFLAGS=-fPIC",
296394
"--prefix",
297-
context.build_paths["prefix_dir"],
395+
prefix,
298396
"--disable-shared",
299397
],
300398
cwd=mpdec_dir,
@@ -306,6 +404,7 @@ def make_mpdec(context, working_dir):
306404
cwd=mpdec_dir,
307405
quiet=context.quiet,
308406
)
407+
write_library_config(prefix, "mpdec", mpdec_config, context.quiet)
309408

310409

311410
@subdir("host_dir", clean_ok=True)
@@ -436,16 +535,24 @@ def make_emscripten_python(context, working_dir):
436535
subprocess.check_call([exec_script, "--version"])
437536

438537

439-
def build_all(context):
440-
"""Build everything."""
441-
steps = [
442-
configure_build_python,
443-
make_build_python,
444-
make_emscripten_libffi,
445-
make_mpdec,
446-
configure_emscripten_python,
447-
make_emscripten_python,
448-
]
538+
def build_target(context):
539+
"""Build one or more targets."""
540+
steps = []
541+
if context.target in {"all"}:
542+
steps.append(install_emscripten)
543+
if context.target in {"build", "all"}:
544+
steps.extend([
545+
configure_build_python,
546+
make_build_python,
547+
])
548+
if context.target in {"host", "all"}:
549+
steps.extend([
550+
make_emscripten_libffi,
551+
make_mpdec,
552+
configure_emscripten_python,
553+
make_emscripten_python,
554+
])
555+
449556
for step in steps:
450557
step(context)
451558

@@ -475,7 +582,22 @@ def main():
475582

476583
parser = argparse.ArgumentParser()
477584
subcommands = parser.add_subparsers(dest="subcommand")
585+
install_emscripten_cmd = subcommands.add_parser(
586+
"install-emscripten",
587+
help="Install the appropriate version of Emscripten",
588+
)
478589
build = subcommands.add_parser("build", help="Build everything")
590+
build.add_argument(
591+
"target",
592+
nargs="?",
593+
default="all",
594+
choices=["all", "host", "build"],
595+
help=(
596+
"What should be built. 'build' for just the build platform, or "
597+
"'host' for the host platform, or 'all' for both. Defaults to 'all'."
598+
),
599+
)
600+
479601
configure_build = subcommands.add_parser(
480602
"configure-build-python", help="Run `configure` for the build Python"
481603
)
@@ -512,6 +634,7 @@ def main():
512634
)
513635

514636
for subcommand in (
637+
install_emscripten_cmd,
515638
build,
516639
configure_build,
517640
make_libffi_cmd,
@@ -568,22 +691,25 @@ def main():
568691

569692
context = parser.parse_args()
570693

571-
context.build_paths = get_build_paths(context.cross_build_dir)
572-
573-
if context.emsdk_cache:
694+
if context.emsdk_cache and context.subcommand != "install-emscripten":
574695
validate_emsdk_version(context.emsdk_cache)
575696
context.emsdk_cache = Path(context.emsdk_cache).absolute()
576697
else:
577698
print("Build will use EMSDK from current environment.")
578699

700+
context.build_paths = get_build_paths(
701+
context.cross_build_dir, context.emsdk_cache
702+
)
703+
579704
dispatch = {
705+
"install-emscripten": install_emscripten,
580706
"make-libffi": make_emscripten_libffi,
581707
"make-mpdec": make_mpdec,
582708
"configure-build-python": configure_build_python,
583709
"make-build-python": make_build_python,
584710
"configure-host": configure_emscripten_python,
585711
"make-host": make_emscripten_python,
586-
"build": build_all,
712+
"build": build_target,
587713
"clean": clean_contents,
588714
}
589715

Tools/wasm/emscripten/config.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Any data that can vary between Python versions is to be kept in this file.
2+
# This allows for blanket copying of the Emscripten build code between supported
3+
# Python versions.
4+
emscripten-version = "4.0.12"
5+
6+
[libffi]
7+
url = "https://github.com/libffi/libffi/releases/download/v{version}/libffi-{version}.tar.gz"
8+
version = "3.4.6"
9+
shasum = "b0dea9df23c863a7a50e825440f3ebffabd65df1497108e5d437747843895a4e"
10+
11+
[mpdec]
12+
url = "https://www.bytereef.org/software/mpdecimal/releases/mpdecimal-{version}.tar.gz"
13+
version = "4.0.1"
14+
shasum = "96d33abb4bb0070c7be0fed4246cd38416188325f820468214471938545b1ac8"

Tools/wasm/emscripten/emscripten_version.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)