44import contextlib
55import functools
66import hashlib
7+ import json
78import os
89import shutil
910import subprocess
1415from textwrap import dedent
1516from urllib .request import urlopen
1617
18+ import tomllib
19+
1720try :
1821 from os import process_cpu_count as cpu_count
1922except ImportError :
2225
2326EMSCRIPTEN_DIR = Path (__file__ ).parent
2427CHECKOUT = EMSCRIPTEN_DIR .parent .parent .parent
25- EMSCRIPTEN_VERSION_FILE = EMSCRIPTEN_DIR / "emscripten_version.txt "
28+ CONFIG_FILE = EMSCRIPTEN_DIR / "config.toml "
2629
2730DEFAULT_CROSS_BUILD_DIR = CHECKOUT / "cross-build"
2831HOST_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
4776LOCAL_SETUP = CHECKOUT / "Modules" / "Setup.local"
4877LOCAL_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-
6380def 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 )
211257def 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 )
262346def 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 )
282373def 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
0 commit comments