comparison third_party/emsdk/emsdk.py @ 186:8cf4ec5e2191 hg-web

Fixed merge conflict.
author MrJuneJune <me@mrjunejune.com>
date Fri, 23 Jan 2026 22:38:59 -0800
parents 8d17f6e6e290
children
comparison
equal deleted inserted replaced
176:fed99fc04e12 186:8cf4ec5e2191
1 #!/usr/bin/env python3
2 # Copyright 2019 The Emscripten Authors. All rights reserved.
3 # Emscripten is available under two separate licenses, the MIT license and the
4 # University of Illinois/NCSA Open Source License. Both these licenses can be
5 # found in the LICENSE file.
6
7 import copy
8 import errno
9 import json
10 import multiprocessing
11 import os
12 import os.path
13 import platform
14 import re
15 import shutil
16 import stat
17 import subprocess
18 import sys
19 import sysconfig
20 import tarfile
21 import zipfile
22 from collections import OrderedDict
23
24 if os.name == 'nt':
25 import ctypes.wintypes
26 import winreg
27
28 from urllib.parse import urljoin
29 from urllib.request import urlopen
30
31 if sys.version_info < (3, 2): # noqa: UP036
32 print(f'error: emsdk requires python 3.2 or above ({sys.executable} {sys.version})', file=sys.stderr)
33 sys.exit(1)
34
35 emsdk_packages_url = 'https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/'
36
37 emscripten_releases_repo = 'https://chromium.googlesource.com/emscripten-releases'
38
39 emscripten_releases_download_url_template = "https://storage.googleapis.com/webassembly/emscripten-releases-builds/%s/%s/wasm-binaries%s.%s"
40
41 # This was previously `master.zip` but we are transitioning to `main` and
42 # `HEAD.zip` works for both cases. In future we could switch this to
43 # `main.zip` perhaps.
44 emsdk_zip_download_url = 'https://github.com/emscripten-core/emsdk/archive/HEAD.zip'
45
46 download_dir = 'downloads/'
47
48 extra_release_tag = None
49
50 # Enable this to do very verbose printing about the different steps that are
51 # being run. Useful for debugging.
52 VERBOSE = int(os.getenv('EMSDK_VERBOSE', '0'))
53 QUIET = int(os.getenv('EMSDK_QUIET', '0'))
54 if os.getenv('EMSDK_NOTTY'):
55 TTY_OUTPUT = False
56 else:
57 TTY_OUTPUT = sys.stdout.isatty()
58
59
60 def info(msg):
61 if not QUIET:
62 print(msg, file=sys.stderr)
63
64
65 def errlog(msg):
66 print(msg, file=sys.stderr)
67
68
69 def exit_with_error(msg):
70 errlog('error: %s' % msg)
71 sys.exit(1)
72
73
74 WINDOWS = False
75 MINGW = False
76 MSYS = False
77 MACOS = False
78 LINUX = False
79
80 os_override = os.environ.get('EMSDK_OS')
81 if os_override:
82 if os_override == 'windows':
83 WINDOWS = True
84 elif os_override == 'linux':
85 LINUX = True
86 elif os_override == 'macos':
87 MACOS = True
88 else:
89 assert False, 'EMSDK_OS must be one of: windows, linux, macos'
90 else:
91 if os.name == 'nt' or ('windows' in os.getenv('SYSTEMROOT', '').lower()) or ('windows' in os.getenv('COMSPEC', '').lower()):
92 WINDOWS = True
93
94 if os.getenv('MSYSTEM'):
95 MSYS = True
96 # Some functions like os.path.normpath() exhibit different behavior between
97 # different versions of Python, so we need to distinguish between the MinGW
98 # and MSYS versions of Python
99 if sysconfig.get_platform() == 'mingw':
100 MINGW = True
101 if os.getenv('MSYSTEM') != 'MSYS' and os.getenv('MSYSTEM') != 'MINGW64':
102 # https://stackoverflow.com/questions/37460073/msys-vs-mingw-internal-environment-variables
103 errlog('Warning: MSYSTEM environment variable is present, and is set to "' + os.getenv('MSYSTEM') + '". This shell has not been tested with emsdk and may not work.')
104
105 if platform.mac_ver()[0] != '':
106 MACOS = True
107
108 if not MACOS and (platform.system() == 'Linux'):
109 LINUX = True
110
111 UNIX = (MACOS or LINUX)
112
113
114 # Pick which shell of 4 shells to use
115 POWERSHELL = bool(os.getenv('EMSDK_POWERSHELL'))
116 CSH = bool(os.getenv('EMSDK_CSH'))
117 CMD = bool(os.getenv('EMSDK_CMD'))
118 BASH = bool(os.getenv('EMSDK_BASH'))
119 FISH = bool(os.getenv('EMSDK_FISH'))
120
121 if WINDOWS and BASH:
122 MSYS = True
123
124 if not CSH and not POWERSHELL and not BASH and not CMD and not FISH:
125 # Fall back to default of `cmd` on windows and `bash` otherwise
126 if WINDOWS and not MSYS:
127 CMD = True
128 else:
129 BASH = True
130
131 if WINDOWS:
132 ENVPATH_SEPARATOR = ';'
133 else:
134 ENVPATH_SEPARATOR = ':'
135
136 # platform.machine() may return AMD64 on windows, so standardize the case.
137 machine = os.getenv('EMSDK_ARCH', platform.machine().lower())
138 if machine.startswith(('x64', 'amd64', 'x86_64')):
139 ARCH = 'x86_64'
140 elif machine.endswith('86'):
141 ARCH = 'x86'
142 elif machine.startswith('aarch64') or machine.lower().startswith('arm64'):
143 ARCH = 'arm64'
144 elif machine.startswith('arm'):
145 ARCH = 'arm'
146 else:
147 exit_with_error('unknown machine architecture: ' + machine)
148
149
150 # Don't saturate all cores to not steal the whole system, but be aggressive.
151 CPU_CORES = int(os.getenv('EMSDK_NUM_CORES', max(multiprocessing.cpu_count() - 1, 1)))
152
153 CMAKE_BUILD_TYPE_OVERRIDE = None
154
155 # If true, perform a --shallow clone of git.
156 GIT_CLONE_SHALLOW = False
157
158 # If true, LLVM backend is built with tests enabled, and Binaryen is built with
159 # Visual Studio static analyzer enabled.
160 BUILD_FOR_TESTING = False
161
162 # If 'auto', assertions are decided by the build type
163 # (Release&MinSizeRel=disabled, Debug&RelWithDebInfo=enabled)
164 # Other valid values are 'ON' and 'OFF'
165 ENABLE_LLVM_ASSERTIONS = 'auto'
166
167 # If true, keeps the downloaded archive files.
168 KEEP_DOWNLOADS = bool(os.getenv('EMSDK_KEEP_DOWNLOADS'))
169
170
171 def os_name():
172 if WINDOWS:
173 return 'win'
174 elif LINUX:
175 return 'linux'
176 elif MACOS:
177 return 'mac'
178 else:
179 raise Exception('unknown OS')
180
181
182 def debug_print(msg):
183 if VERBOSE:
184 errlog(msg)
185
186
187 def to_unix_path(p):
188 return p.replace('\\', '/')
189
190
191 EMSDK_PATH = to_unix_path(os.path.dirname(os.path.realpath(__file__)))
192
193 EMSDK_SET_ENV = ""
194 if POWERSHELL:
195 EMSDK_SET_ENV = os.path.join(EMSDK_PATH, 'emsdk_set_env.ps1')
196 else:
197 EMSDK_SET_ENV = os.path.join(EMSDK_PATH, 'emsdk_set_env.bat')
198
199
200 # Parses https://github.com/emscripten-core/emscripten/tree/d6aced8 to a triplet
201 # (https://github.com/emscripten-core/emscripten, d6aced8, emscripten-core)
202 # or https://github.com/emscripten-core/emscripten/commit/00b76f81f6474113fcf540db69297cfeb180347e
203 # to (https://github.com/emscripten-core/emscripten, 00b76f81f6474113fcf540db69297cfeb180347e, emscripten-core)
204 def parse_github_url_and_refspec(url):
205 if not url:
206 return ('', '', None)
207
208 if url.endswith(('/tree/', '/tree', '/commit/', '/commit')):
209 raise Exception(f'Malformed git URL and refspec {url}!')
210
211 if '/tree/' in url:
212 if url.endswith('/'):
213 raise Exception(f'Malformed git URL and refspec {url}!')
214 url, refspec = url.split('/tree/')
215 remote_name = url.split('/')[-2]
216 return (url, refspec, remote_name)
217 elif '/commit/' in url:
218 if url.endswith('/'):
219 raise Exception(f'Malformed git URL and refspec {url}!')
220 url, refspec = url.split('/commit/')
221 remote_name = url.split('/')[-2]
222 return (url, refspec, remote_name)
223 else:
224 return (url, 'main', None) # Assume the default branch is main in the absence of a refspec
225
226
227 ARCHIVE_SUFFIXES = ('zip', '.tar', '.gz', '.xz', '.tbz2', '.bz2')
228
229
230 # Finds the given executable 'program' in PATH. Operates like the Unix tool 'which'.
231 def which(program):
232 def is_exe(fpath):
233 return os.path.isfile(fpath) and (WINDOWS or os.access(fpath, os.X_OK))
234
235 fpath, fname = os.path.split(program)
236 if fpath:
237 if is_exe(program):
238 return program
239 else:
240 for path in os.environ["PATH"].split(os.pathsep):
241 path = path.strip('"')
242 exe_file = os.path.join(path, program)
243 if is_exe(exe_file):
244 return exe_file
245
246 if WINDOWS and '.' not in fname:
247 if is_exe(exe_file + '.exe'):
248 return exe_file + '.exe'
249 if is_exe(exe_file + '.cmd'):
250 return exe_file + '.cmd'
251 if is_exe(exe_file + '.bat'):
252 return exe_file + '.bat'
253
254 return None
255
256
257 def vswhere(version):
258 try:
259 program_files = os.getenv('ProgramFiles(x86)')
260 if not program_files:
261 program_files = os.environ['ProgramFiles']
262 vswhere_path = os.path.join(program_files, 'Microsoft Visual Studio', 'Installer', 'vswhere.exe')
263 # Source: https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools?view=vs-2022
264 tools_arch = 'ARM64' if ARCH == 'arm64' else 'x86.x64'
265 # The "-products *" allows detection of Build Tools, the "-prerelease" allows detection of Preview version
266 # of Visual Studio and Build Tools.
267 output = json.loads(subprocess.check_output([vswhere_path, '-latest', '-products', '*', '-prerelease', '-version', '[%s.0,%s.0)' % (version, version + 1), '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.' + tools_arch, '-property', 'installationPath', '-format', 'json']))
268 return str(output[0]['installationPath'])
269 except Exception:
270 return ''
271
272
273 def vs_filewhere(installation_path, platform, file):
274 try:
275 vcvarsall = os.path.join(installation_path, 'VC\\Auxiliary\\Build\\vcvarsall.bat')
276 env = subprocess.check_output('cmd /c "%s" %s & where %s' % (vcvarsall, platform, file))
277 paths = [path[:-len(file)] for path in env.split('\r\n') if path.endswith(file)]
278 return paths[0]
279 except Exception:
280 return ''
281
282
283 CMAKE_GENERATOR = 'Unix Makefiles'
284 if WINDOWS:
285 # Detect which CMake generator to use when building on Windows
286 if '--mingw' in sys.argv:
287 CMAKE_GENERATOR = 'MinGW Makefiles'
288 elif '--vs2022' in sys.argv:
289 CMAKE_GENERATOR = 'Visual Studio 17'
290 elif '--vs2019' in sys.argv:
291 CMAKE_GENERATOR = 'Visual Studio 16'
292 elif len(vswhere(17)) > 0:
293 CMAKE_GENERATOR = 'Visual Studio 17'
294 elif len(vswhere(16)) > 0:
295 CMAKE_GENERATOR = 'Visual Studio 16'
296 elif which('mingw32-make') is not None and which('g++') is not None:
297 CMAKE_GENERATOR = 'MinGW Makefiles'
298 else:
299 # No detected generator
300 CMAKE_GENERATOR = ''
301
302
303 sys.argv = [a for a in sys.argv if a not in ('--mingw', '--vs2019', '--vs2022')]
304
305
306 # Computes a suitable path prefix to use when building with a given generator.
307 def cmake_generator_prefix():
308 if CMAKE_GENERATOR == 'Visual Studio 17':
309 return '_vs2022'
310 if CMAKE_GENERATOR == 'Visual Studio 16':
311 return '_vs2019'
312 elif CMAKE_GENERATOR == 'MinGW Makefiles':
313 return '_mingw'
314 # Unix Makefiles do not specify a path prefix for backwards path compatibility
315 return ''
316
317
318 # Removes a directory tree even if it was readonly, and doesn't throw exception
319 # on failure.
320 def remove_tree(d):
321 debug_print('remove_tree(' + str(d) + ')')
322 if not os.path.exists(d):
323 return
324 try:
325 def remove_readonly_and_try_again(func, path, exc_info):
326 if not (os.stat(path).st_mode & stat.S_IWRITE):
327 os.chmod(path, stat.S_IWRITE)
328 func(path)
329 else:
330 raise exc_info[1]
331 shutil.rmtree(d, onerror=remove_readonly_and_try_again)
332 except Exception as e:
333 debug_print('remove_tree threw an exception, ignoring: ' + str(e))
334
335
336 def win_set_environment_variable_direct(key, value, system=True):
337 folder = None
338 try:
339 if system:
340 # Read globally from ALL USERS section.
341 folder = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 0, winreg.KEY_ALL_ACCESS)
342 else:
343 # Register locally from CURRENT USER section.
344 folder = winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, 'Environment', 0, winreg.KEY_ALL_ACCESS)
345 winreg.SetValueEx(folder, key, 0, winreg.REG_EXPAND_SZ, value)
346 debug_print(f'Set key={key} with value {value} in registry.')
347 return True
348 except Exception as e:
349 # 'Access is denied.'
350 if e.args[3] == 5:
351 exit_with_error(f'failed to set the environment variable \'{key}\'! Setting environment variables permanently requires administrator access. Please rerun this command with administrative privileges. This can be done for example by holding down the Ctrl and Shift keys while opening a command prompt in start menu.')
352 errlog(f'Failed to write environment variable {key}:')
353 errlog(str(e))
354 return False
355 finally:
356 if folder is not None:
357 folder.Close()
358
359
360 def win_get_environment_variable(key, system=True, user=True, fallback=True):
361 if (not system and not user and fallback):
362 # if no --system or --permanent flag is provided use shell's value
363 return os.environ[key]
364 try:
365 folder = None
366 try:
367 if system:
368 # Read globally from ALL USERS section.
369 folder = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment')
370 else:
371 # Register locally from CURRENT USER section.
372 folder = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Environment')
373 value = str(winreg.QueryValueEx(folder, key)[0])
374 except Exception:
375 # If reading registry fails for some reason - read via os.environ. This has the drawback
376 # that expansion items such as %PROGRAMFILES% will have been expanded, so
377 # need to be precise not to set these back to system registry, or
378 # expansion items would be lost.
379 if fallback:
380 return os.environ[key]
381 return None
382 finally:
383 if folder is not None:
384 folder.Close()
385
386 except Exception as e:
387 # this catch is if both the registry key threw an exception and the key is not in os.environ
388 if e.args[0] != 2:
389 # 'The system cannot find the file specified.'
390 errlog(f'Failed to read environment variable {key}:')
391 errlog(str(e))
392 return None
393 return value
394
395
396 def win_set_environment_variable(key, value, system, user):
397 debug_print('set ' + str(key) + '=' + str(value) + ', in system=' + str(system))
398 previous_value = win_get_environment_variable(key, system=system, user=user)
399 if previous_value == value:
400 debug_print(' no need to set, since same value already exists.')
401 # No need to elevate UAC for nothing to set the same value, skip.
402 return False
403
404 if not value:
405 try:
406 if system:
407 cmd = ['REG', 'DELETE', 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', '/V', key, '/f']
408 else:
409 cmd = ['REG', 'DELETE', 'HKCU\\Environment', '/V', key, '/f']
410 debug_print(str(cmd))
411 value = subprocess.call(cmd, stdout=subprocess.PIPE)
412 except Exception:
413 return False
414 return True
415
416 try:
417 if win_set_environment_variable_direct(key, value, system):
418 return True
419 # Escape % signs so that we don't expand references to environment variables.
420 value = value.replace('%', '^%')
421 if len(value) >= 1024:
422 exit_with_error(f'the new environment variable {key} is more than 1024 characters long! A value this long cannot be set via command line: please add the environment variable specified above to system environment manually via Control Panel.')
423 cmd = ['SETX', key, value]
424 debug_print(str(cmd))
425 retcode = subprocess.call(cmd, stdout=subprocess.PIPE)
426 if retcode != 0:
427 errlog(f'ERROR! Failed to set environment variable {key}={value}. You may need to set it manually.')
428 else:
429 return True
430 except Exception as e:
431 errlog(f'ERROR! Failed to set environment variable {key}={value}:')
432 errlog(str(e))
433 errlog('You may need to set it manually.')
434
435 return False
436
437
438 def win_set_environment_variables(env_vars_to_add, system, user):
439 if not env_vars_to_add:
440 return
441
442 changed = False
443
444 for key, value in env_vars_to_add:
445 if win_set_environment_variable(key, value, system, user):
446 if not changed:
447 changed = True
448 print('Setting global environment variables:')
449
450 print(key + ' = ' + value)
451
452 if not changed:
453 print('Global environment variables up to date')
454 return
455
456 # if changes were made then we need to notify other processes
457 try:
458 HWND_BROADCAST = ctypes.wintypes.HWND(0xFFFF) # win32con.HWND_BROADCAST == 65535
459 WM_SETTINGCHANGE = 0x001A # win32con.WM_SETTINGCHANGE == 26
460 SMTO_BLOCK = 0x0001 # win32con.SMTO_BLOCK == 1
461 ctypes.windll.user32.SendMessageTimeoutA(
462 HWND_BROADCAST, # hWnd: notify everyone
463 WM_SETTINGCHANGE, # Msg: registry changed
464 0, # wParam: Must be 0 when setting changed is sent by users
465 'Environment', # lParam: Specifically environment variables changed
466 SMTO_BLOCK, # fuFlags: Wait for message to be sent or timeout
467 100) # uTimeout: 100ms
468 except Exception as e:
469 errlog('SendMessageTimeout failed with error: ' + str(e))
470
471
472 def win_delete_environment_variable(key, system=True, user=True):
473 debug_print(f'win_delete_environment_variable(key={key}, system={system})')
474 return win_set_environment_variable(key, None, system, user)
475
476
477 # Returns the absolute pathname to the given path inside the Emscripten SDK.
478 def sdk_path(path):
479 if os.path.isabs(path):
480 return path
481
482 return to_unix_path(os.path.join(EMSDK_PATH, path))
483
484
485 # Removes a single file, suppressing exceptions on failure.
486 def rmfile(filename):
487 debug_print(f'rmfile({filename})')
488 if os.path.lexists(filename):
489 os.remove(filename)
490
491
492 def mkdir_p(path):
493 debug_print(f'mkdir_p({path})')
494 os.makedirs(path, exist_ok=True)
495
496
497 def is_nonempty_directory(path):
498 if not os.path.isdir(path):
499 return False
500 return len(os.listdir(path)) != 0
501
502
503 def run(cmd, cwd=None, quiet=False):
504 debug_print('run(cmd=' + str(cmd) + ', cwd=' + str(cwd) + ')')
505 process = subprocess.Popen(cmd, cwd=cwd, env=os.environ.copy())
506 process.communicate()
507 if process.returncode != 0 and not quiet:
508 errlog(str(cmd) + ' failed with error code ' + str(process.returncode) + '!')
509 return process.returncode
510
511
512 # http://pythonicprose.blogspot.fi/2009/10/python-extract-targz-archive.html
513 def untargz(source_filename, dest_dir):
514 print(f"Unpacking '{source_filename}' to '{dest_dir}'")
515 mkdir_p(dest_dir)
516 returncode = run(['tar', '-xvf' if VERBOSE else '-xf', sdk_path(source_filename), '--strip', '1'], cwd=dest_dir)
517 # tfile = tarfile.open(source_filename, 'r:gz')
518 # tfile.extractall(dest_dir)
519 return returncode == 0
520
521
522 # On Windows, it is not possible to reference path names that are longer than
523 # ~260 characters, unless the path is referenced via a "\\?\" prefix.
524 # See https://msdn.microsoft.com/en-us/library/aa365247.aspx#maxpath and http://stackoverflow.com/questions/3555527/python-win32-filename-length-workaround
525 # In that mode, forward slashes cannot be used as delimiters.
526 def fix_potentially_long_windows_pathname(pathname):
527 if (not WINDOWS or MSYS) or os_override:
528 return pathname
529 # Test if emsdk calls fix_potentially_long_windows_pathname() with long
530 # relative paths (which is problematic)
531 if not os.path.isabs(pathname) and len(pathname) > 200:
532 errlog(f'Warning: Seeing a relative path "{pathname}" which is dangerously long for being referenced as a short Windows path name. Refactor emsdk to be able to handle this!')
533 if pathname.startswith('\\\\?\\'):
534 return pathname
535 pathname = os.path.normpath(pathname.replace('/', '\\'))
536 if MINGW:
537 # MinGW versions of Python return normalized paths with backslashes
538 # converted to forward slashes, so we must use forward slashes in our
539 # prefix
540 return '//?/' + pathname
541 return '\\\\?\\' + pathname
542
543
544 # On windows, rename/move will fail if the destination exists, and there is no
545 # race-free way to do it. This method removes the destination if it exists, so
546 # the move always works
547 def move_with_overwrite(src, dest):
548 if os.path.exists(dest):
549 os.remove(dest)
550 os.rename(src, dest)
551
552
553 # http://stackoverflow.com/questions/12886768/simple-way-to-unzip-file-in-python-on-all-oses
554 def unzip(source_filename, dest_dir):
555 print(f"Unpacking '{source_filename}' to '{dest_dir}'")
556 mkdir_p(dest_dir)
557 common_subdir = None
558 try:
559 with zipfile.ZipFile(source_filename) as zf:
560 # Implement '--strip 1' behavior to unzipping by testing if all the files
561 # in the zip reside in a common subdirectory, and if so, we move the
562 # output tree at the end of uncompression step.
563 for member in zf.infolist():
564 words = member.filename.split('/')
565 if len(words) > 1: # If there is a directory component?
566 if common_subdir is None:
567 common_subdir = words[0]
568 elif common_subdir != words[0]:
569 common_subdir = None
570 break
571 else:
572 common_subdir = None
573 break
574
575 unzip_to_dir = dest_dir
576 if common_subdir:
577 debug_print(f'common_subdir: {common_subdir}')
578 unzip_to_dir = os.path.join(os.path.dirname(dest_dir), 'unzip_temp')
579
580 # Now do the actual decompress.
581 for member in zf.infolist():
582 zf.extract(member, fix_potentially_long_windows_pathname(unzip_to_dir))
583 dst_filename = os.path.join(unzip_to_dir, member.filename)
584
585 # See: https://stackoverflow.com/questions/42326428/zipfile-in-python-file-permission
586 unix_attributes = member.external_attr >> 16
587 if unix_attributes:
588 os.chmod(dst_filename, unix_attributes)
589
590 # Move the extracted file to its final location without the base
591 # directory name, if we are stripping that away.
592 if common_subdir:
593 assert member.filename.startswith(common_subdir), f'unexpected filename {member.filename}'
594 stripped_filename = '.' + member.filename[len(common_subdir):]
595 final_dst_filename = os.path.join(dest_dir, stripped_filename)
596 # Check if a directory
597 if stripped_filename.endswith('/'):
598 d = fix_potentially_long_windows_pathname(final_dst_filename)
599 if not os.path.isdir(d):
600 os.mkdir(d)
601 else:
602 parent_dir = os.path.dirname(fix_potentially_long_windows_pathname(final_dst_filename))
603 if parent_dir and not os.path.exists(parent_dir):
604 os.makedirs(parent_dir)
605 move_with_overwrite(fix_potentially_long_windows_pathname(dst_filename), fix_potentially_long_windows_pathname(final_dst_filename))
606
607 if common_subdir:
608 remove_tree(unzip_to_dir)
609 except zipfile.BadZipfile as e:
610 errlog(f"Unzipping file '{source_filename}' failed due to reason: {e}! Removing the corrupted zip file.")
611 rmfile(source_filename)
612 return False
613 except Exception as e:
614 errlog(f"Unzipping file '{source_filename}' failed due to reason: {e}")
615 return False
616
617 return True
618
619
620 # This function interprets whether the given string looks like a path to a
621 # directory instead of a file, without looking at the actual filesystem.
622 # 'a/b/c' points to directory, so does 'a/b/c/', but 'a/b/c.x' is parsed as a
623 # filename
624 def path_points_to_directory(path):
625 if path == '.':
626 return True
627 last_slash = max(path.rfind('/'), path.rfind('\\'))
628 last_dot = path.rfind('.')
629 no_suffix = last_dot < last_slash or last_dot == -1
630 if no_suffix:
631 return True
632 suffix = path[last_dot:]
633 # Very simple logic for the only file suffixes used by emsdk downloader. Other
634 # suffixes, like 'clang-3.2' are treated as dirs.
635 if suffix in ('.exe', '.zip', '.txt'):
636 return False
637 else:
638 return True
639
640
641 def get_content_length(download):
642 try:
643 meta = download.info()
644 if hasattr(meta, "getheaders") and hasattr(meta.getheaders, "Content-Length"):
645 return int(meta.getheaders("Content-Length")[0])
646 elif hasattr(download, "getheader") and download.getheader('Content-Length'):
647 return int(download.getheader('Content-Length'))
648 elif hasattr(meta, "getheader") and meta.getheader('Content-Length'):
649 return int(meta.getheader('Content-Length'))
650 except Exception:
651 pass
652
653 return 0
654
655
656 def get_download_target(url, dstpath, filename_prefix=''):
657 file_name = filename_prefix + url.split('/')[-1]
658 if path_points_to_directory(dstpath):
659 file_name = os.path.join(dstpath, file_name)
660 else:
661 file_name = dstpath
662
663 # Treat all relative destination paths as relative to the SDK root directory,
664 # not the current working directory.
665 file_name = sdk_path(file_name)
666
667 return file_name
668
669
670 def download_with_curl(url, file_name):
671 print("Downloading: %s from %s" % (file_name, url))
672 if not which('curl'):
673 exit_with_error('curl not found in PATH')
674 # -#: show progress bar
675 # -L: Follow HTTP 3XX redirections
676 # -f: Fail on HTTP errors
677 subprocess.check_call(['curl', '-#', '-f', '-L', '-o', file_name, url])
678
679
680 def download_with_urllib(url, file_name):
681 u = urlopen(url)
682 file_size = get_content_length(u)
683 if file_size > 0:
684 print("Downloading: %s from %s, %s Bytes" % (file_name, url, file_size))
685 else:
686 print("Downloading: %s from %s" % (file_name, url))
687
688 file_size_dl = 0
689 # Draw a progress bar 80 chars wide (in non-TTY mode)
690 progress_max = 80 - 4
691 progress_shown = 0
692 block_sz = 256 * 1024
693 if not TTY_OUTPUT:
694 print(' [', end='')
695
696 with open(file_name, 'wb') as f:
697 while True:
698 buffer = u.read(block_sz)
699 if not buffer:
700 break
701
702 file_size_dl += len(buffer)
703 f.write(buffer)
704 if file_size:
705 percent = file_size_dl * 100.0 / file_size
706 if TTY_OUTPUT:
707 status = r" %10d [%3.02f%%]" % (file_size_dl, percent)
708 print(status, end='\r')
709 else:
710 while progress_shown < progress_max * percent / 100:
711 print('-', end='')
712 sys.stdout.flush()
713 progress_shown += 1
714
715 if not TTY_OUTPUT:
716 print(']')
717 sys.stdout.flush()
718
719 debug_print('finished downloading (%d bytes)' % file_size_dl)
720
721
722 # On success, returns the filename on the disk pointing to the destination file that was produced
723 # On failure, returns None.
724 def download_file(url, dstpath, download_even_if_exists=False,
725 filename_prefix=''):
726 debug_print(f'download_file(url={url}, dstpath={dstpath})')
727 file_name = get_download_target(url, dstpath, filename_prefix)
728
729 if os.path.exists(file_name) and not download_even_if_exists:
730 print(f"File '{file_name}' already downloaded, skipping.")
731 return file_name
732
733 mkdir_p(os.path.dirname(file_name))
734
735 try:
736 # Use curl on macOS to avoid CERTIFICATE_VERIFY_FAILED issue with
737 # python's urllib:
738 # https://stackoverflow.com/questions/40684543/how-to-make-python-use-ca-certificates-from-mac-os-truststore
739 # Unlike on linux or windows, curl is always available on macOS systems.
740 if MACOS:
741 download_with_curl(url, file_name)
742 else:
743 download_with_urllib(url, file_name)
744 except Exception as e:
745 errlog(f"Error: Downloading URL '{url}': {e}")
746 return None
747 except KeyboardInterrupt:
748 rmfile(file_name)
749 raise
750
751 return file_name
752
753
754 def run_get_output(cmd, cwd=None):
755 debug_print(f'run_get_output(cmd={cmd}, cwd={cwd})')
756 process = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, env=os.environ.copy(), universal_newlines=True)
757 stdout, stderr = process.communicate()
758 return (process.returncode, stdout, stderr)
759
760
761 cached_git_executable = None
762
763
764 # must_succeed: If false, the search is performed silently without printing out
765 # errors if not found. Empty string is returned if git is not found.
766 # If true, the search is required to succeed, and the execution
767 # will terminate with sys.exit(1) if not found.
768 def GIT(must_succeed=True):
769 global cached_git_executable
770 if cached_git_executable is not None:
771 return cached_git_executable
772 # The order in the following is important, and specifies the preferred order
773 # of using the git tools. Primarily use git from emsdk if installed. If not,
774 # use system git.
775 gits = ['git/1.9.4/bin/git.exe', which('git')]
776 for git in gits:
777 try:
778 ret, stdout, stderr = run_get_output([git, '--version'])
779 if ret == 0:
780 cached_git_executable = git
781 return git
782 except Exception:
783 pass
784 if must_succeed:
785 if WINDOWS:
786 msg = "git executable was not found. Please install it by typing 'emsdk install git-1.9.4', or alternatively by installing it manually from http://git-scm.com/downloads . If you install git manually, remember to add it to PATH"
787 elif MACOS:
788 msg = "git executable was not found. Please install git for this operation! This can be done from http://git-scm.com/ , or by installing XCode and then the XCode Command Line Tools (see http://stackoverflow.com/questions/9329243/xcode-4-4-command-line-tools )"
789 elif LINUX:
790 msg = "git executable was not found. Please install git for this operation! This can be probably be done using your package manager, see http://git-scm.com/book/en/Getting-Started-Installing-Git"
791 else:
792 msg = "git executable was not found. Please install git for this operation!"
793 exit_with_error(msg)
794 # Not found
795 return ''
796
797
798 def git_repo_version(repo_path):
799 returncode, stdout, stderr = run_get_output([GIT(), 'log', '-n', '1', '--pretty="%aD %H"'], cwd=repo_path)
800 if returncode == 0:
801 return stdout.strip()
802 else:
803 return ""
804
805
806 def git_recent_commits(repo_path, n=20):
807 returncode, stdout, stderr = run_get_output([GIT(), 'log', '-n', str(n), '--pretty="%H"'], cwd=repo_path)
808 if returncode == 0:
809 return stdout.strip().replace('\r', '').replace('"', '').split('\n')
810 else:
811 return []
812
813
814 def get_git_remotes(repo_path):
815 remotes = []
816 output = subprocess.check_output([GIT(), 'remote', '-v'], stderr=subprocess.STDOUT, text=True, cwd=repo_path)
817 for line in output.splitlines():
818 remotes += [line.split()[0]]
819 return remotes
820
821
822 def git_clone(url, dstpath, branch, remote_name='origin'):
823 debug_print(f'git_clone(url={url}, dstpath={dstpath})')
824 if os.path.isdir(os.path.join(dstpath, '.git')):
825 remotes = get_git_remotes(dstpath)
826 if remote_name in remotes:
827 debug_print(f'Repository {url} with remote "{remote_name}" already cloned to directory {dstpath}, skipping.')
828 return True
829 else:
830 debug_print(f'Repository {url} with remote "{remote_name}" already cloned to directory {dstpath}, but remote has not yet been added. Creating.')
831 return run([GIT(), 'remote', 'add', remote_name, url], cwd=dstpath) == 0
832
833 mkdir_p(dstpath)
834 git_clone_args = ['--recurse-submodules', '--branch', branch] # Do not check out a branch (installer will issue a checkout command right after)
835 if GIT_CLONE_SHALLOW:
836 git_clone_args += ['--depth', '1']
837 print(f'Cloning from {url}...')
838 return run([GIT(), 'clone', '-o', remote_name] + git_clone_args + [url, dstpath]) == 0
839
840
841 def git_pull(repo_path, branch_or_tag, remote_name='origin'):
842 debug_print(f'git_pull(repo_path={repo_path}, branch/tag={branch_or_tag}, remote_name={remote_name})')
843 ret = run([GIT(), 'fetch', '--quiet', remote_name], repo_path)
844 if ret != 0:
845 return False
846 try:
847 print(f"Fetching latest changes to the branch/tag '{branch_or_tag}' for '{repo_path}'...")
848 ret = run([GIT(), 'fetch', '--quiet', remote_name], repo_path)
849 if ret != 0:
850 return False
851 # Test if branch_or_tag is a branch, or if it is a tag that needs to be updated
852 target_is_tag = run([GIT(), 'symbolic-ref', '-q', 'HEAD'], repo_path, quiet=True)
853
854 if target_is_tag:
855 ret = run([GIT(), 'checkout', '--recurse-submodules', '--quiet', branch_or_tag], repo_path)
856 else:
857 local_branch_prefix = (remote_name + '_') if remote_name != 'origin' else ''
858 ret = run([GIT(), 'checkout', '--recurse-submodules', '--quiet', '-B', local_branch_prefix + branch_or_tag,
859 '--track', remote_name + '/' + branch_or_tag], repo_path)
860 if ret != 0:
861 return False
862 if not target_is_tag:
863 # update branch to latest (not needed for tags)
864 # this line assumes that the user has not gone and made local changes to the repo
865 ret = run([GIT(), 'merge', '--ff-only', remote_name + '/' + branch_or_tag], repo_path)
866 if ret != 0:
867 return False
868 run([GIT(), 'submodule', 'update', '--init'], repo_path, quiet=True)
869 except Exception:
870 errlog('git operation failed!')
871 return False
872 print(f"Successfully updated and checked out branch/tag '{branch_or_tag}' on repository '{repo_path}'")
873 print("Current repository version: " + git_repo_version(repo_path))
874 return True
875
876
877 def git_clone_checkout_and_pull(url, dstpath, branch, override_remote_name='origin'):
878 debug_print(f'git_clone_checkout_and_pull(url={url}, dstpath={dstpath}, branch={branch}, override_remote_name={override_remote_name})')
879
880 # Make sure the repository is cloned first
881 success = git_clone(url, dstpath, branch, override_remote_name)
882 if not success:
883 return False
884
885 # And/or issue a pull/checkout to get to latest code.
886 return git_pull(dstpath, branch, override_remote_name)
887
888
889 # Each tool can have its own build type, or it can be overridden on the command
890 # line.
891 def decide_cmake_build_type(tool):
892 if CMAKE_BUILD_TYPE_OVERRIDE:
893 return CMAKE_BUILD_TYPE_OVERRIDE
894 else:
895 return tool.cmake_build_type
896
897
898 # The root directory of the build.
899 def llvm_build_dir(tool):
900 generator_suffix = cmake_generator_prefix()
901 bitness_suffix = '_32' if tool.bitness == 32 else '_64'
902
903 if hasattr(tool, 'git_branch'):
904 build_dir = 'build_' + tool.git_branch.replace(os.sep, '-') + generator_suffix + bitness_suffix
905 else:
906 build_dir = 'build_' + tool.version + generator_suffix + bitness_suffix
907 return build_dir
908
909
910 def exe_suffix(filename):
911 if WINDOWS and not filename.endswith('.exe'):
912 filename += '.exe'
913 return filename
914
915
916 # The directory where the binaries are produced. (relative to the installation
917 # root directory of the tool)
918 def llvm_build_bin_dir(tool):
919 build_dir = llvm_build_dir(tool)
920 if WINDOWS and 'Visual Studio' in CMAKE_GENERATOR:
921 old_llvm_bin_dir = os.path.join(build_dir, 'bin', decide_cmake_build_type(tool))
922
923 new_llvm_bin_dir = None
924 default_cmake_build_type = decide_cmake_build_type(tool)
925 cmake_build_types = [default_cmake_build_type, 'Release', 'RelWithDebInfo', 'MinSizeRel', 'Debug']
926 for build_type in cmake_build_types:
927 d = os.path.join(build_dir, build_type, 'bin')
928 if os.path.isfile(os.path.join(tool.installation_path(), d, exe_suffix('clang'))):
929 new_llvm_bin_dir = d
930 break
931
932 if new_llvm_bin_dir and os.path.exists(os.path.join(tool.installation_path(), new_llvm_bin_dir)):
933 return new_llvm_bin_dir
934 elif os.path.exists(os.path.join(tool.installation_path(), old_llvm_bin_dir)):
935 return old_llvm_bin_dir
936 return os.path.join(build_dir, default_cmake_build_type, 'bin')
937 else:
938 return os.path.join(build_dir, 'bin')
939
940
941 def build_env():
942 env = os.environ.copy()
943
944 # To work around a build issue with older Mac OS X builds, add -stdlib=libc++ to all builds.
945 # See https://groups.google.com/forum/#!topic/emscripten-discuss/5Or6QIzkqf0
946 if MACOS:
947 env['CXXFLAGS'] = ((env['CXXFLAGS'] + ' ') if hasattr(env, 'CXXFLAGS') else '') + '-stdlib=libc++'
948 if WINDOWS:
949 # MSBuild.exe has an internal mechanism to avoid N^2 oversubscription of threads in its two-tier build model, see
950 # https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/
951 env['UseMultiToolTask'] = 'true'
952 env['EnforceProcessCountAcrossBuilds'] = 'true'
953 return env
954
955
956 # Find path to cmake executable, as one of the activated tools, in PATH, or from installed tools.
957 def find_cmake():
958 def locate_cmake_from_tool(tool):
959 tool_path = get_required_path([tool])
960 tool_path = tool_path[-1]
961 cmake_exe = 'cmake.exe' if WINDOWS else 'cmake'
962 cmake_exe = os.path.join(tool_path, cmake_exe)
963 if os.path.isfile(cmake_exe):
964 return cmake_exe
965
966 # 1. If user has activated a specific cmake tool, then use that tool to configure the build.
967 for tool in reversed(tools):
968 if tool.id == 'cmake' and tool.is_active():
969 cmake_exe = locate_cmake_from_tool(tool)
970 if cmake_exe:
971 info(f'Found installed+activated CMake tool at "{cmake_exe}"')
972 return cmake_exe
973
974 # 2. If cmake already exists in PATH, then use that cmake to configure the build.
975 cmake_exe = which('cmake')
976 if cmake_exe:
977 info(f'Found CMake from PATH at "{cmake_exe}"')
978 return cmake_exe
979
980 # 3. Finally, if user has installed a cmake tool, but has not activated that, then use
981 # that tool. This enables a single-liner directive
982 # "emsdk install cmake-4.2.0-rc3-64bit llvm-git-main-64bit" to first install CMake, and
983 # then use it to configure to build LLVM.
984 for tool in reversed(tools):
985 if tool.id == 'cmake' and tool.is_installed():
986 cmake_exe = locate_cmake_from_tool(tool)
987 if cmake_exe:
988 info(f'Found installed CMake tool at "{cmake_exe}"')
989 return cmake_exe
990
991 exit_with_error('Unable to find "cmake" in PATH, or as installed/activated tool! Please install CMake first')
992
993
994 def make_build(build_root, build_type):
995 debug_print(f'make_build(build_root={build_root}, build_type={build_type})')
996 if CPU_CORES > 1:
997 print('Performing a parallel build with ' + str(CPU_CORES) + ' cores.')
998 else:
999 print('Performing a singlethreaded build.')
1000
1001 make = [find_cmake(), '--build', '.', '--config', build_type]
1002 if 'Visual Studio' in CMAKE_GENERATOR:
1003 # Visual Studio historically has had a two-tier problem in its build system design. A single MSBuild.exe instance only governs
1004 # the build of a single project (.exe/.lib/.dll) in a solution. Passing the -j parameter above will only enable multiple MSBuild.exe
1005 # instances to be spawned to build multiple projects in parallel, but each MSBuild.exe is still singlethreaded.
1006 # To enable each MSBuild.exe instance to also compile several .cpp files in parallel inside a single project, pass the extra
1007 # MSBuild.exe specific "Multi-ToolTask" (MTT) setting /p:CL_MPCount. This enables each MSBuild.exe to parallelize builds wide.
1008 # This requires CMake 3.12 or newer.
1009 make += ['-j', str(CPU_CORES), '--', '/p:CL_MPCount=' + str(CPU_CORES)]
1010 else:
1011 # Pass -j to native make, CMake might not support -j option.
1012 make += ['--', '-j', str(CPU_CORES)]
1013
1014 # Build
1015 try:
1016 print('Running build: ' + str(make))
1017 ret = subprocess.check_call(make, cwd=build_root, env=build_env())
1018 if ret != 0:
1019 errlog('Build failed with exit code {ret}!')
1020 errlog('Working directory: ' + build_root)
1021 return False
1022 except Exception as e:
1023 errlog('Build failed due to exception!')
1024 errlog('Working directory: ' + build_root)
1025 errlog(str(e))
1026 return False
1027
1028 return True
1029
1030
1031 def cmake_configure(generator, build_root, src_root, build_type, extra_cmake_args):
1032 debug_print('cmake_configure(generator=' + str(generator) + ', build_root=' + str(build_root) + ', src_root=' + str(src_root) + ', build_type=' + str(build_type) + ', extra_cmake_args=' + str(extra_cmake_args) + ')')
1033 # Configure
1034 if not os.path.isdir(build_root):
1035 # Create build output directory if it doesn't yet exist.
1036 os.mkdir(build_root)
1037 try:
1038 if generator:
1039 generator = ['-G', generator]
1040 else:
1041 generator = []
1042
1043 cmdline = [find_cmake()] + generator + ['-DCMAKE_BUILD_TYPE=' + build_type, '-DPYTHON_EXECUTABLE=' + sys.executable]
1044 # Target macOS 11.0 Big Sur at minimum, to support older Mac devices.
1045 # See https://en.wikipedia.org/wiki/MacOS#Hardware_compatibility for min-spec details.
1046 cmdline += ['-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0']
1047 cmdline += extra_cmake_args + [src_root]
1048
1049 print('Running CMake: ' + str(cmdline))
1050
1051 # Specify the deployment target also as an env. var, since some Xcode versions
1052 # read this instead of the CMake field.
1053 os.environ['MACOSX_DEPLOYMENT_TARGET'] = '11.0'
1054
1055 def quote_parens(x):
1056 if ' ' in x:
1057 return '"' + x.replace('"', '\\"') + '"'
1058 else:
1059 return x
1060
1061 # Create a file 'recmake.bat/sh' in the build root that user can call to
1062 # manually recmake the build tree with the previous build params
1063 open(os.path.join(build_root, 'recmake.' + ('bat' if WINDOWS else 'sh')), 'w').write(' '.join(map(quote_parens, cmdline)))
1064 ret = subprocess.check_call(cmdline, cwd=build_root, env=build_env())
1065 if ret != 0:
1066 errlog('CMake invocation failed with exit code {ret}!')
1067 errlog('Working directory: ' + build_root)
1068 return False
1069 except OSError as e:
1070 if e.errno == errno.ENOENT:
1071 errlog(str(e))
1072 errlog('Could not run CMake, perhaps it has not been installed?')
1073 if WINDOWS:
1074 errlog('Installing this package requires CMake. Get it from http://www.cmake.org/')
1075 elif LINUX:
1076 errlog('Installing this package requires CMake. Get it via your system package manager (e.g. sudo apt-get install cmake), or from http://www.cmake.org/')
1077 elif MACOS:
1078 errlog('Installing this package requires CMake. Get it via a macOS package manager (Homebrew: "brew install cmake", or MacPorts: "sudo port install cmake"), or from http://www.cmake.org/')
1079 return False
1080 raise
1081 except Exception as e:
1082 errlog('CMake invocation failed due to exception!')
1083 errlog('Working directory: ' + build_root)
1084 errlog(str(e))
1085 return False
1086
1087 return True
1088
1089
1090 def xcode_sdk_version():
1091 try:
1092 output = subprocess.check_output(['xcrun', '--show-sdk-version'], universal_newlines=True)
1093 return output.strip().split('.')
1094 except Exception:
1095 return subprocess.checkplatform.mac_ver()[0].split('.')
1096
1097
1098 def cmake_target_platform(tool):
1099 # Source: https://cmake.org/cmake/help/latest/generator/Visual%20Studio%2017%202022.html#platform-selection
1100 if hasattr(tool, 'arch'):
1101 if tool.arch == 'arm64':
1102 return 'ARM64'
1103 elif tool.arch == 'x86_64':
1104 return 'x64'
1105 elif tool.arch == 'x86':
1106 return 'Win32'
1107 if ARCH == 'arm64':
1108 return 'ARM64'
1109 else:
1110 return 'x64' if tool.bitness == 64 else 'Win32'
1111
1112
1113 def cmake_host_platform():
1114 # Source: https://cmake.org/cmake/help/latest/generator/Visual%20Studio%2017%202022.html#toolset-selection
1115 arch_to_cmake_host_platform = {
1116 'arm64': 'ARM64',
1117 'arm': 'ARM',
1118 'x86_64': 'x64',
1119 'x86': 'x86',
1120 }
1121 return arch_to_cmake_host_platform[ARCH]
1122
1123
1124 def get_generator_and_config_args(tool):
1125 args = []
1126 cmake_generator = CMAKE_GENERATOR
1127 if 'Visual Studio 16' in CMAKE_GENERATOR or 'Visual Studio 17' in CMAKE_GENERATOR: # VS2019 or VS2022
1128 # With Visual Studio 16 2019, CMake changed the way they specify target arch.
1129 # Instead of appending it into the CMake generator line, it is specified
1130 # with a -A arch parameter.
1131 args += ['-A', cmake_target_platform(tool)]
1132 args += ['-Thost=' + cmake_host_platform()]
1133 elif 'Visual Studio' in CMAKE_GENERATOR and tool.bitness == 64:
1134 cmake_generator += ' Win64'
1135 args += ['-Thost=x64']
1136 return (cmake_generator, args)
1137
1138
1139 def build_llvm(tool):
1140 debug_print('build_llvm(' + str(tool) + ')')
1141 llvm_root = tool.installation_path()
1142 llvm_src_root = os.path.join(llvm_root, 'src')
1143 success = git_clone_checkout_and_pull(tool.download_url(), llvm_src_root, tool.git_branch)
1144 if not success:
1145 return False
1146
1147 build_dir = llvm_build_dir(tool)
1148 build_root = os.path.join(llvm_root, build_dir)
1149
1150 build_type = decide_cmake_build_type(tool)
1151
1152 # Configure
1153 tests_arg = 'ON' if BUILD_FOR_TESTING else 'OFF'
1154
1155 enable_assertions = ENABLE_LLVM_ASSERTIONS.lower() == 'on' or (ENABLE_LLVM_ASSERTIONS == 'auto' and build_type.lower() != 'release' and build_type.lower() != 'minsizerel')
1156
1157 if ARCH in ('x86', 'x86_64'):
1158 targets_to_build = 'WebAssembly;X86'
1159 elif ARCH == 'arm':
1160 targets_to_build = 'WebAssembly;ARM'
1161 elif ARCH == 'arm64':
1162 targets_to_build = 'WebAssembly;AArch64'
1163 else:
1164 targets_to_build = 'WebAssembly'
1165 cmake_generator, args = get_generator_and_config_args(tool)
1166 args += ['-DLLVM_TARGETS_TO_BUILD=' + targets_to_build,
1167 '-DLLVM_INCLUDE_EXAMPLES=OFF',
1168 '-DLLVM_INCLUDE_TESTS=' + tests_arg,
1169 '-DCLANG_INCLUDE_TESTS=' + tests_arg,
1170 '-DLLVM_ENABLE_ASSERTIONS=' + ('ON' if enable_assertions else 'OFF'),
1171 # Disable optional LLVM dependencies, these can cause unwanted .so dependencies
1172 # that prevent distributing the generated compiler for end users.
1173 '-DLLVM_ENABLE_LIBXML2=OFF', '-DLLVM_ENABLE_TERMINFO=OFF', '-DLLDB_ENABLE_LIBEDIT=OFF',
1174 '-DLLVM_ENABLE_LIBEDIT=OFF', '-DLLVM_ENABLE_LIBPFM=OFF']
1175 # LLVM build system bug: compiler-rt does not build on Windows. It insists on performing a CMake install step that writes to C:\Program Files. Attempting
1176 # to reroute that to build_root directory then fails on an error
1177 # file INSTALL cannot find
1178 # "C:/code/emsdk/llvm/git/build_master_vs2017_64/$(Configuration)/lib/clang/10.0.0/lib/windows/clang_rt.ubsan_standalone-x86_64.lib".
1179 # (there instead of $(Configuration), one would need ${CMAKE_BUILD_TYPE} ?)
1180 # It looks like compiler-rt is not compatible to build on Windows?
1181 args += ['-DLLVM_ENABLE_PROJECTS=clang;lld']
1182 # To enable widest possible chance of success for building, let the code
1183 # compile through with older toolchains that are about to be deprecated by
1184 # upstream LLVM.
1185 args += ['-DLLVM_TEMPORARILY_ALLOW_OLD_TOOLCHAIN=ON']
1186
1187 if os.getenv('LLVM_CMAKE_ARGS'):
1188 extra_args = os.environ['LLVM_CMAKE_ARGS'].split(',')
1189 print('Passing the following extra arguments to LLVM CMake configuration: ' + str(extra_args))
1190 args += extra_args
1191
1192 cmakelists_dir = os.path.join(llvm_src_root, 'llvm')
1193 success = cmake_configure(cmake_generator, build_root, cmakelists_dir, build_type, args)
1194 if not success:
1195 return False
1196
1197 # Make
1198 success = make_build(build_root, build_type)
1199 return success
1200
1201
1202 def build_ninja(tool):
1203 debug_print('build_ninja(' + str(tool) + ')')
1204 root = os.path.normpath(tool.installation_path())
1205 src_root = os.path.join(root, 'src')
1206 success = git_clone_checkout_and_pull(tool.download_url(), src_root, tool.git_branch)
1207 if not success:
1208 return False
1209
1210 build_dir = llvm_build_dir(tool)
1211 build_root = os.path.join(root, build_dir)
1212
1213 build_type = decide_cmake_build_type(tool)
1214
1215 # Configure
1216 cmake_generator, args = get_generator_and_config_args(tool)
1217
1218 cmakelists_dir = os.path.join(src_root)
1219 success = cmake_configure(cmake_generator, build_root, cmakelists_dir, build_type, args)
1220 if not success:
1221 return False
1222
1223 # Make
1224 success = make_build(build_root, build_type)
1225
1226 if success:
1227 bin_dir = os.path.join(root, 'bin')
1228 mkdir_p(bin_dir)
1229 exe_paths = [os.path.join(build_root, 'Release', 'ninja'), os.path.join(build_root, 'ninja')]
1230 for e in exe_paths:
1231 for s in ['.exe', '']:
1232 ninja = e + s
1233 if os.path.isfile(ninja):
1234 dst = os.path.join(bin_dir, 'ninja' + s)
1235 shutil.copyfile(ninja, dst)
1236 os.chmod(dst, os.stat(dst).st_mode | stat.S_IEXEC)
1237
1238 return success
1239
1240
1241 def build_ccache(tool):
1242 debug_print('build_ccache(' + str(tool) + ')')
1243 root = os.path.normpath(tool.installation_path())
1244 src_root = os.path.join(root, 'src')
1245 success = git_clone_checkout_and_pull(tool.download_url(), src_root, tool.git_branch)
1246 if not success:
1247 return False
1248
1249 build_dir = llvm_build_dir(tool)
1250 build_root = os.path.join(root, build_dir)
1251
1252 build_type = decide_cmake_build_type(tool)
1253
1254 # Configure
1255 cmake_generator, args = get_generator_and_config_args(tool)
1256 args += ['-DZSTD_FROM_INTERNET=ON']
1257
1258 cmakelists_dir = os.path.join(src_root)
1259 success = cmake_configure(cmake_generator, build_root, cmakelists_dir, build_type, args)
1260 if not success:
1261 return False
1262
1263 # Make
1264 success = make_build(build_root, build_type)
1265
1266 if success:
1267 bin_dir = os.path.join(root, 'bin')
1268 mkdir_p(bin_dir)
1269 exe_paths = [os.path.join(build_root, 'Release', 'ccache'), os.path.join(build_root, 'ccache')]
1270 for e in exe_paths:
1271 for s in ['.exe', '']:
1272 ccache = e + s
1273 if os.path.isfile(ccache):
1274 dst = os.path.join(bin_dir, 'ccache' + s)
1275 shutil.copyfile(ccache, dst)
1276 os.chmod(dst, os.stat(dst).st_mode | stat.S_IEXEC)
1277
1278 cache_dir = os.path.join(root, 'cache')
1279 open(os.path.join(root, 'emcc_ccache.conf'), 'w').write('''# Set maximum cache size to 10 GB:
1280 max_size = 10G
1281 cache_dir = %s
1282 ''' % cache_dir)
1283 mkdir_p(cache_dir)
1284
1285 return success
1286
1287
1288 def download_firefox(tool):
1289 debug_print('download_firefox(' + str(tool) + ')')
1290
1291 # Use mozdownload to acquire Firefox versions.
1292 try:
1293 from mozdownload import FactoryScraper
1294 except ImportError:
1295 # If mozdownload is not available, invoke pip to install it.
1296 subprocess.check_call([sys.executable, "-m", "pip", "install", "mozdownload"])
1297 from mozdownload import FactoryScraper
1298
1299 if WINDOWS:
1300 extension = 'exe'
1301 elif MACOS:
1302 extension = 'dmg'
1303 else:
1304 # N.b. on Linux even when we ask .tar.xz, we might sometimes get .tar.bz2,
1305 # depending on what is available on Firefox servers for the particular
1306 # version. So prepare to handle both further down below.
1307 extension = 'tar.xz'
1308
1309 platform = None
1310 if LINUX and 'arm' in ARCH:
1311 platform = 'linux-arm64'
1312 if WINDOWS and 'arm' in ARCH:
1313 platform = 'win64-aarch64'
1314
1315 if tool.version == 'nightly':
1316 scraper = FactoryScraper('daily', extension=extension, locale='en-US', platform=platform)
1317 else:
1318 scraper = FactoryScraper('release', extension=extension, locale='en-US', platform=platform, version=tool.version)
1319
1320 if tool.version == 'nightly':
1321 firefox_version = os.path.basename(scraper.filename).split(".en-US")[0]
1322 else:
1323 firefox_version = os.path.basename(scraper.filename).split("firefox-")[1].split(".en-US")[0]
1324
1325 print('Target Firefox version: ' + firefox_version)
1326 if tool.version in ['latest', 'latest-esr', 'latest-beta', 'nightly']:
1327 pretend_version_dir = os.path.normpath(tool.installation_path())
1328 orig_version = tool.version
1329 tool.version = firefox_version
1330 root = os.path.normpath(tool.installation_path())
1331 tool.version = orig_version
1332 else:
1333 pretend_version_dir = None
1334 root = os.path.normpath(tool.installation_path())
1335
1336 # For moving installer packages, e.g. "nightly", "latest", "latest-esr",
1337 # store a text file to specify the actual installation directory.
1338 def save_actual_version():
1339 if os.path.isfile(firefox_exe) and pretend_version_dir:
1340 print(pretend_version_dir)
1341 os.makedirs(pretend_version_dir, exist_ok=True)
1342 open(os.path.join(pretend_version_dir, 'actual.txt'), 'w').write(os.path.relpath(root, EMSDK_PATH))
1343
1344 # Check if already installed
1345 print('Firefox installation root directory: ' + root)
1346 exe_dir = os.path.join(root, 'Contents', 'MacOS') if MACOS else root
1347 firefox_exe = os.path.join(exe_dir, exe_suffix('firefox'))
1348 if os.path.isfile(firefox_exe):
1349 print(firefox_exe + ' is already installed, skipping..')
1350 save_actual_version()
1351 return True
1352
1353 print('Downloading Firefox from ' + scraper.url)
1354 filename = scraper.download()
1355 print('Finished downloading ' + filename)
1356
1357 if not MACOS:
1358 os.makedirs(root, exist_ok=True)
1359
1360 if extension == 'exe':
1361 # Uncompress the NSIS installer to 'install' Firefox
1362 run(['C:\\Program Files\\7-Zip\\7z.exe', 'x', '-y', filename, '-o' + root])
1363
1364 if '.tar.' in filename:
1365 if filename.endswith('.tar.bz2'):
1366 tar_type = 'r:bz2'
1367 elif filename.endswith('.tar.xz'):
1368 tar_type = 'r:xz'
1369 else:
1370 raise Exception('Unknown archive type!')
1371
1372 with tarfile.open(filename, tar_type) as tar:
1373 tar.extractall(path=root)
1374 collapse_subdir = os.path.join(root, 'firefox')
1375
1376 elif filename.endswith('.dmg'):
1377 mount_point = '/Volumes/Firefox Nightly' if tool.version == 'nightly' else '/Volumes/Firefox'
1378 app_name = 'Firefox Nightly.app' if tool.version == 'nightly' else 'Firefox.app'
1379
1380 # If a previous mount point exists, detach it first
1381 if os.path.exists(mount_point):
1382 run(['hdiutil', 'detach', mount_point])
1383
1384 # Abort if detaching was not successful
1385 if os.path.exists(mount_point):
1386 raise Exception(f'Previous mount of Firefox already exists at "{mount_point}", unable to proceed.')
1387
1388 # Mount the archive
1389 run(['hdiutil', 'attach', filename])
1390 firefox_dir = os.path.join(mount_point, app_name)
1391 if not os.path.isdir(firefox_dir):
1392 raise Exception(f'Unable to find Firefox directory "{firefox_dir}" inside app image.')
1393
1394 # And install by copying the files from the archive
1395 shutil.copytree(firefox_dir, root)
1396 run(['hdiutil', 'detach', mount_point])
1397 collapse_subdir = None
1398
1399 elif filename.endswith('.exe'):
1400 # NSIS installer package has a core/ directory, remove it as redundant.
1401 collapse_subdir = os.path.join(root, 'core')
1402
1403 # Remove a redundant subdirectory by moving installed files up one directory.
1404 if collapse_subdir and os.path.isdir(collapse_subdir):
1405 # Rename the parent subdirectory first, since we will be handling a nested `firefox/firefox/`
1406 collapse = collapse_subdir + '_temp_renamed'
1407 os.rename(collapse_subdir, collapse)
1408
1409 # Move all files up by one directory
1410 for f in os.listdir(collapse):
1411 shutil.move(os.path.join(collapse, f), os.path.dirname(collapse))
1412
1413 # The root directory should now be empty
1414 os.rmdir(collapse)
1415
1416 # Original installer is now done.
1417 os.remove(filename)
1418
1419 # Write a policy file that prevents Firefox from auto-updating itself.
1420 if MACOS:
1421 distribution_path = os.path.join(root, 'Contents', 'Resources', 'distribution')
1422 else:
1423 distribution_path = os.path.join(root, 'distribution')
1424 os.makedirs(distribution_path, exist_ok=True)
1425 open(os.path.join(distribution_path, 'policies.json'), 'w').write('''{
1426 "policies": {
1427 "AppAutoUpdate": false,
1428 "DisableAppUpdate": true
1429 }
1430 }''')
1431
1432 if MACOS:
1433 # Disable a macOS feature where if the browser is terminated mid-execution, (e.g. by
1434 # CI aborting), then the next time the browser is launched, macOS might bring up a dialog
1435 # "The last time you opened Firefox, it unexpectedly quit while reopening windows.
1436 # Do you want to try to reopen its windows again?"
1437 # that will block automated CI runs.
1438 # Disable this feature by changing the behavior of the program with macOS 'defaults'.
1439 run(['defaults', 'write', '-app', root, 'ApplePersistenceIgnoreState', 'YES'])
1440 run(['defaults', 'write', '-app', root, 'NSQuitAlwaysKeepsWindows', '-bool', 'false'])
1441
1442 save_actual_version()
1443
1444 # If we didn't get a Firefox executable, then installation failed.
1445 return os.path.isfile(firefox_exe)
1446
1447
1448 def is_firefox_installed(tool):
1449 actual_file = os.path.join(tool.installation_dir(), 'actual.txt')
1450 if not os.path.isfile(actual_file):
1451 return False
1452
1453 actual_installation_dir = sdk_path(open(actual_file).read())
1454 exe_dir = os.path.join(actual_installation_dir, 'Contents', 'MacOS') if MACOS else actual_installation_dir
1455 firefox_exe = os.path.join(exe_dir, exe_suffix('firefox'))
1456 return os.path.isfile(firefox_exe)
1457
1458
1459 # Finds the newest installed version of a given tool
1460 def find_latest_installed_tool(name):
1461 for t in reversed(tools):
1462 if t.id == name and t.is_installed():
1463 return t
1464
1465
1466 # npm install in Emscripten root directory
1467 def emscripten_npm_install(tool, directory):
1468 node_tool = find_latest_installed_tool('node')
1469 if not node_tool:
1470 npm_fallback = which('npm')
1471 if not npm_fallback:
1472 errlog('Failed to find npm command!')
1473 errlog('Running "npm ci" in installed Emscripten root directory ' + tool.installation_path() + ' is required!')
1474 errlog('Please install node.js first!')
1475 return False
1476 node_path = os.path.dirname(npm_fallback)
1477 else:
1478 node_path = os.path.join(node_tool.installation_path(), 'bin')
1479
1480 npm = os.path.join(node_path, 'npm' + ('.cmd' if WINDOWS else ''))
1481 env = os.environ.copy()
1482 env["PATH"] = node_path + os.pathsep + env["PATH"]
1483 print('Running post-install step: npm ci ...')
1484 try:
1485 subprocess.check_output(
1486 [npm, 'ci', '--production'],
1487 cwd=directory, stderr=subprocess.STDOUT, env=env,
1488 universal_newlines=True)
1489 except subprocess.CalledProcessError as e:
1490 errlog('Error running %s:\n%s' % (e.cmd, e.output))
1491 return False
1492
1493 print('Done running: npm ci')
1494
1495 if os.path.isfile(os.path.join(directory, 'bootstrap.py')):
1496 try:
1497 subprocess.check_output([sys.executable, os.path.join(directory, 'bootstrap.py')],
1498 cwd=directory, stderr=subprocess.STDOUT, env=env,
1499 universal_newlines=True)
1500 except subprocess.CalledProcessError as e:
1501 errlog('Error running %s:\n%s' % (e.cmd, e.output))
1502 return False
1503
1504 print('Done running: Emscripten bootstrap')
1505 return True
1506
1507
1508 # Binaryen build scripts:
1509 def binaryen_build_root(tool):
1510 build_root = tool.installation_path().strip()
1511 if build_root.endswith(('/', '\\')):
1512 build_root = build_root[:-1]
1513 generator_prefix = cmake_generator_prefix()
1514 build_root = build_root + generator_prefix + '_' + str(tool.bitness) + 'bit_binaryen'
1515 return build_root
1516
1517
1518 def uninstall_binaryen(tool):
1519 debug_print('uninstall_binaryen(' + str(tool) + ')')
1520 build_root = binaryen_build_root(tool)
1521 print(f"Deleting path '{build_root}'")
1522 remove_tree(build_root)
1523
1524
1525 def is_binaryen_installed(tool):
1526 build_root = binaryen_build_root(tool)
1527 return os.path.exists(build_root)
1528
1529
1530 def build_binaryen_tool(tool):
1531 debug_print('build_binaryen_tool(' + str(tool) + ')')
1532 src_root = tool.installation_path()
1533 build_root = binaryen_build_root(tool)
1534 build_type = decide_cmake_build_type(tool)
1535
1536 # Configure
1537 cmake_generator, args = get_generator_and_config_args(tool)
1538 args += ['-DENABLE_WERROR=0'] # -Werror is not useful for end users
1539 args += ['-DBUILD_TESTS=0'] # We don't want to build or run tests
1540
1541 if 'Visual Studio' in CMAKE_GENERATOR:
1542 if BUILD_FOR_TESTING:
1543 args += ['-DRUN_STATIC_ANALYZER=1']
1544
1545 success = cmake_configure(cmake_generator, build_root, src_root, build_type, args)
1546 if not success:
1547 return False
1548
1549 # Make
1550 success = make_build(build_root, build_type)
1551
1552 # Deploy scripts needed from source repository to build directory
1553 remove_tree(os.path.join(build_root, 'scripts'))
1554 shutil.copytree(os.path.join(src_root, 'scripts'), os.path.join(build_root, 'scripts'))
1555 remove_tree(os.path.join(build_root, 'src', 'js'))
1556 shutil.copytree(os.path.join(src_root, 'src', 'js'), os.path.join(build_root, 'src', 'js'))
1557
1558 return success
1559
1560
1561 def download_and_extract(archive, dest_dir, filename_prefix='', clobber=True):
1562 debug_print('download_and_extract(archive={archive}, dest_dir={dest_dir})')
1563
1564 url = urljoin(emsdk_packages_url, archive)
1565
1566 def try_download(url):
1567 return download_file(url, download_dir, not KEEP_DOWNLOADS, filename_prefix)
1568
1569 # Special hack for the wasm-binaries we transitioned from `.bzip2` to
1570 # `.xz`, but we can't tell from the version/url which one to use, so
1571 # try one and then fall back to the other.
1572 success = False
1573 if 'wasm-binaries' in archive and os.path.splitext(archive)[1] == '.xz':
1574 success = try_download(url)
1575 if not success:
1576 alt_url = url.replace('.tar.xz', '.tbz2')
1577 success = try_download(alt_url)
1578 if success:
1579 url = alt_url
1580
1581 if not success:
1582 success = try_download(url)
1583
1584 if not success:
1585 return False
1586
1587 # Remove the old directory, since we have some SDKs that install into the
1588 # same directory. If we didn't do this contents of the previous install
1589 # could remain.
1590 if clobber:
1591 remove_tree(dest_dir)
1592
1593 download_target = get_download_target(url, download_dir, filename_prefix)
1594 if archive.endswith('.zip'):
1595 return unzip(download_target, dest_dir)
1596 else:
1597 return untargz(download_target, dest_dir)
1598
1599
1600 def to_native_path(p):
1601 if (WINDOWS and not MSYS) and not os_override:
1602 return to_unix_path(p).replace('/', '\\')
1603 else:
1604 return to_unix_path(p)
1605
1606
1607 # Finds and returns a list of the directories that need to be added to PATH for
1608 # the given set of tools.
1609 def get_required_path(active_tools):
1610 path_add = [to_native_path(EMSDK_PATH)]
1611 for tool in active_tools:
1612 if hasattr(tool, 'activated_path'):
1613 path = to_native_path(tool.expand_vars(tool.activated_path))
1614 # If the tool has an activated_path_skip attribute then we don't add
1615 # the tools path to the users path if a program by that name is found
1616 # in the existing PATH. This allows us to, for example, add our version
1617 # node to the users PATH if, and only if, they don't already have a
1618 # another version of node in their PATH.
1619 if hasattr(tool, 'activated_path_skip'):
1620 current_path = which(tool.activated_path_skip)
1621 # We found an executable by this name in the current PATH, but we
1622 # ignore our own version for this purpose.
1623 if current_path and os.path.dirname(current_path) != path:
1624 continue
1625 path_add.append(path)
1626 return path_add
1627
1628
1629 # Returns the absolute path to the file '.emscripten' for the current user on
1630 # this system.
1631 EM_CONFIG_PATH = os.path.join(EMSDK_PATH, ".emscripten")
1632 EM_CONFIG_DICT = {}
1633
1634
1635 def parse_key_value(line):
1636 if not line:
1637 return ('', '')
1638 eq = line.find('=')
1639 if eq != -1:
1640 key = line[0:eq].strip()
1641 value = line[eq + 1:].strip()
1642 return (key, value)
1643 else:
1644 return (key, '')
1645
1646
1647 def load_em_config():
1648 EM_CONFIG_DICT.clear()
1649 lines = []
1650 try:
1651 lines = open(EM_CONFIG_PATH).read().split('\n')
1652 except Exception:
1653 pass
1654 for line in lines:
1655 try:
1656 key, value = parse_key_value(line)
1657 if value != '':
1658 EM_CONFIG_DICT[key] = value
1659 except Exception:
1660 pass
1661
1662
1663 def find_emscripten_root(active_tools):
1664 """Find the currently active emscripten root.
1665
1666 If there is more than one tool that defines EMSCRIPTEN_ROOT (this
1667 should not happen under normal circumstances), assume the last one takes
1668 precedence.
1669 """
1670 root = None
1671 for tool in active_tools:
1672 config = tool.activated_config()
1673 if 'EMSCRIPTEN_ROOT' in config:
1674 root = config['EMSCRIPTEN_ROOT']
1675 return root
1676
1677
1678 def fetch_nightly_node_versions():
1679 # Node.js Apple ARM64 nightly downloads are currently out of order, so pin
1680 # to recent version that does still exist. https://github.com/nodejs/node/issues/59654
1681 if MACOS and ARCH == 'arm64':
1682 return ['v25.0.0-nightly20250715b305119844']
1683
1684 url = "https://nodejs.org/download/nightly/"
1685 with urlopen(url) as response:
1686 html = response.read().decode("utf-8")
1687
1688 # Regex to capture href values like v7.0.0-nightly2016080175c6d9dd95/
1689 pattern = re.compile(r'<a href="(v[0-9]+\.[0-9]+\.[0-9]+-nightly[0-9a-f]+)/">')
1690 matches = pattern.findall(html)
1691 return matches
1692
1693
1694 def dir_installed_nightly_node_versions():
1695 path = os.path.abspath('node')
1696 try:
1697 return [name for name in os.listdir(path) if os.path.isdir(os.path.join(path, name)) and name.startswith("nightly-")]
1698 except Exception:
1699 return []
1700
1701
1702 def extract_newest_node_nightly_version(versions):
1703 def parse(v):
1704 # example: v7.0.0-nightly2016080175c6d9dd95
1705 m = re.match(r'v(\d+)\.(\d+)\.(\d+)-nightly(\d+)', v)
1706 if m:
1707 major, minor, patch, nightly = m.groups()
1708 return [int(major), int(minor), int(patch), int(nightly)]
1709 else:
1710 return []
1711
1712 try:
1713 return max(versions, key=lambda v: parse(v))
1714 except Exception:
1715 return None
1716
1717
1718 def download_node_nightly(tool):
1719 nightly_versions = fetch_nightly_node_versions()
1720 latest_nightly = extract_newest_node_nightly_version(nightly_versions)
1721 print('Latest Node.js Nightly download available is "{latest_nightly}"')
1722
1723 output_dir = os.path.abspath('node/nightly-' + latest_nightly)
1724 # Node.js zip structure quirk: Linux and macOS archives have a /bin,
1725 # Windows does not. Unify the file structures.
1726 if WINDOWS:
1727 output_dir += '/bin'
1728
1729 if os.path.isdir(output_dir):
1730 return True
1731
1732 url = tool.url.replace('%version%', latest_nightly)
1733 if WINDOWS:
1734 os_ = 'win'
1735 elif LINUX:
1736 os_ = 'linux'
1737 elif MACOS:
1738 os_ = 'darwin'
1739 else:
1740 os_ = ''
1741 if platform.machine().lower() in ["x86_64", "amd64"]:
1742 arch = 'x64'
1743 elif platform.machine().lower() in ["arm64", "aarch64"]:
1744 arch = 'arm64'
1745 if WINDOWS:
1746 zip_suffix = 'zip'
1747 else:
1748 zip_suffix = 'tar.gz'
1749 url = url.replace('%os%', os_)
1750 url = url.replace('%arch%', arch)
1751 url = url.replace('%zip_suffix%', zip_suffix)
1752 download_and_extract(url, output_dir)
1753 open(tool.get_version_file_path(), 'w').write('node-nightly-64bit')
1754 return True
1755
1756
1757 # returns a tuple (string,string) of config files paths that need to used
1758 # to activate emsdk env depending on $SHELL, defaults to bash.
1759 def get_emsdk_shell_env_configs():
1760 default_emsdk_env = sdk_path('emsdk_env.sh')
1761 default_shell_config_file = '$HOME/.bash_profile'
1762 shell = os.getenv('SHELL', '')
1763 if 'zsh' in shell:
1764 return (default_emsdk_env, '$HOME/.zprofile')
1765 elif 'csh' in shell:
1766 return (sdk_path('emsdk_env.csh'), '$HOME/.cshrc')
1767 elif 'fish' in shell:
1768 return (sdk_path('emsdk_env.fish'), '$HOME/.config/fish/config.fish')
1769 else:
1770 return (default_emsdk_env, default_shell_config_file)
1771
1772
1773 def generate_em_config(active_tools, permanently_activate, system):
1774 cfg = 'import os\n'
1775 cfg += "emsdk_path = os.path.dirname(os.getenv('EM_CONFIG')).replace('\\\\', '/')\n"
1776
1777 # Different tools may provide the same activated configs; the latest to be
1778 # activated is the relevant one.
1779 activated_config = OrderedDict()
1780 for tool in active_tools:
1781 for name, value in tool.activated_config().items():
1782 activated_config[name] = value
1783
1784 if 'NODE_JS' not in activated_config:
1785 node_fallback = which('nodejs')
1786 if not node_fallback:
1787 node_fallback = 'node'
1788 activated_config['NODE_JS'] = node_fallback
1789
1790 for name, value in activated_config.items():
1791 if value.startswith('['):
1792 cfg += f'{name} = {value}\n'
1793 else:
1794 cfg += f"{name} = '{value}'\n"
1795
1796 emroot = find_emscripten_root(active_tools)
1797 if emroot:
1798 version = parse_emscripten_version(emroot)
1799 # Older emscripten versions of emscripten depend on certain config
1800 # keys that are no longer used.
1801 # See https://github.com/emscripten-core/emscripten/pull/9469
1802 if version < [1, 38, 46]:
1803 cfg += 'COMPILER_ENGINE = NODE_JS\n'
1804 # See https://github.com/emscripten-core/emscripten/pull/9542
1805 if version < [1, 38, 48]:
1806 cfg += 'JS_ENGINES = [NODE_JS]\n'
1807
1808 cfg = cfg.replace("'" + EMSDK_PATH, "emsdk_path + '")
1809
1810 if os.path.exists(EM_CONFIG_PATH):
1811 backup_path = EM_CONFIG_PATH + ".old"
1812 move_with_overwrite(EM_CONFIG_PATH, backup_path)
1813
1814 with open(EM_CONFIG_PATH, "w") as text_file:
1815 text_file.write(cfg)
1816
1817 # Clear old emscripten content.
1818 rmfile(os.path.join(EMSDK_PATH, ".emscripten_sanity"))
1819
1820 path_add = get_required_path(active_tools)
1821
1822 # Give some recommended next step, depending on the platform
1823 if WINDOWS:
1824 if not permanently_activate and not system:
1825 print('Next steps:')
1826 print('- Consider running `emsdk activate` with --permanent or --system')
1827 print(' to have emsdk settings available on startup.')
1828 else:
1829 print('Next steps:')
1830 print('- To conveniently access emsdk tools from the command line,')
1831 print(' consider adding the following directories to your PATH:')
1832 for p in path_add:
1833 print(' ' + p)
1834 print('- This can be done for the current shell by running:')
1835 emsdk_env, shell_config_file = get_emsdk_shell_env_configs()
1836 print(' source "%s"' % emsdk_env)
1837 print('- Configure emsdk in your shell startup scripts by running:')
1838 print(' echo \'source "%s"\' >> %s' % (emsdk_env, shell_config_file))
1839
1840
1841 def find_msbuild_dir():
1842 program_files = os.getenv('ProgramFiles', 'C:/Program Files')
1843 program_files_x86 = os.getenv('ProgramFiles(x86)', 'C:/Program Files (x86)')
1844 MSBUILDX86_DIR = os.path.join(program_files_x86, "MSBuild/Microsoft.Cpp/v4.0/Platforms")
1845 MSBUILD_DIR = os.path.join(program_files, "MSBuild/Microsoft.Cpp/v4.0/Platforms")
1846 if os.path.exists(MSBUILDX86_DIR):
1847 return MSBUILDX86_DIR
1848 if os.path.exists(MSBUILD_DIR):
1849 return MSBUILD_DIR
1850 # No MSbuild installed.
1851 return ''
1852
1853
1854 class Tool:
1855 def __init__(self, data):
1856 # Convert the dictionary representation of the tool in 'data' to members of
1857 # this class for convenience.
1858 for key, value in data.items():
1859 setattr(self, key, value)
1860
1861 # Cache the name ID of this Tool (these are read very often)
1862 self.name = self.id
1863 if self.version:
1864 self.name += '-' + self.version
1865 if hasattr(self, 'bitness'):
1866 self.name += '-' + str(self.bitness) + 'bit'
1867
1868 def __str__(self):
1869 return self.name
1870
1871 def __repr__(self):
1872 return self.name
1873
1874 def expand_vars(self, str):
1875 if WINDOWS and '%MSBuildPlatformsDir%' in str:
1876 str = str.replace('%MSBuildPlatformsDir%', find_msbuild_dir())
1877 if '%cmake_build_type_on_win%' in str:
1878 str = str.replace('%cmake_build_type_on_win%', (decide_cmake_build_type(self) + '/') if WINDOWS else '')
1879 if '%installation_dir%' in str:
1880 str = str.replace('%installation_dir%', sdk_path(self.installation_dir()))
1881 if '%macos_app_bundle_prefix%' in str:
1882 str = str.replace('%macos_app_bundle_prefix%', 'Contents/MacOS/' if MACOS else '')
1883 if '%actual_installation_dir%' in str:
1884 actual_file = os.path.join(self.installation_dir(), 'actual.txt')
1885 if os.path.isfile(actual_file):
1886 str = str.replace('%actual_installation_dir%', sdk_path(open(actual_file).read()))
1887 else:
1888 str = str.replace('%actual_installation_dir%', '__NOT_INSTALLED__')
1889 if '%generator_prefix%' in str:
1890 str = str.replace('%generator_prefix%', cmake_generator_prefix())
1891 str = str.replace('%.exe%', '.exe' if WINDOWS else '')
1892 if '%llvm_build_bin_dir%' in str:
1893 str = str.replace('%llvm_build_bin_dir%', llvm_build_bin_dir(self))
1894 if '%latest_downloaded_node_nightly_dir%' in str:
1895 installed_node_nightlys = dir_installed_nightly_node_versions()
1896 latest_node_nightly = extract_newest_node_nightly_version(installed_node_nightlys)
1897 if latest_node_nightly:
1898 str = str.replace('%latest_downloaded_node_nightly_dir%', latest_node_nightly)
1899
1900 return str
1901
1902 # Return true if this tool requires building from source, and false if this is a precompiled tool.
1903 def needs_compilation(self):
1904 if hasattr(self, 'cmake_build_type'):
1905 return True
1906
1907 if hasattr(self, 'uses'):
1908 for tool_name in self.uses:
1909 tool = find_tool(tool_name)
1910 if not tool:
1911 debug_print(f'Tool {self} depends on {tool_name} which does not exist!')
1912 continue
1913 if tool.needs_compilation():
1914 return True
1915
1916 return False
1917
1918 # Specifies the target path where this tool will be installed to. This could
1919 # either be a directory or a filename (e.g. in case of node.js)
1920 def installation_path(self):
1921 if hasattr(self, 'install_path'):
1922 pth = self.expand_vars(self.install_path)
1923 return sdk_path(pth)
1924 p = self.version
1925 if hasattr(self, 'bitness') and (not hasattr(self, 'append_bitness') or self.append_bitness):
1926 p += '_' + str(self.bitness) + 'bit'
1927 return sdk_path(os.path.join(self.id, p))
1928
1929 # Specifies the target directory this tool will be installed to.
1930 def installation_dir(self):
1931 dir = self.installation_path()
1932 if path_points_to_directory(dir):
1933 return dir
1934 else:
1935 return os.path.dirname(dir)
1936
1937 # Returns the configuration item that needs to be added to .emscripten to make
1938 # this Tool active for the current user.
1939 def activated_config(self):
1940 if hasattr(self, 'activated_cfg'):
1941 activated_cfg = self.activated_cfg
1942 else:
1943 return {}
1944
1945 config = OrderedDict()
1946 expanded = to_unix_path(self.expand_vars(activated_cfg))
1947 for specific_cfg in expanded.split(';'):
1948 name, value = specific_cfg.split('=')
1949 config[name] = value.strip("'")
1950 return config
1951
1952 def activated_environment(self):
1953 if hasattr(self, 'activated_env'):
1954 activated_env = self.activated_env
1955 else:
1956 return []
1957
1958 return self.expand_vars(activated_env).split(';')
1959
1960 def compatible_with_this_arch(self):
1961 if hasattr(self, 'arch'):
1962 if self.arch != ARCH:
1963 return False
1964 return True
1965
1966 def compatible_with_this_os(self):
1967 if hasattr(self, 'os'):
1968 if self.os == 'all':
1969 return True
1970 if self.compatible_with_this_arch() and ((WINDOWS and 'win' in self.os) or (LINUX and ('linux' in self.os or 'unix' in self.os)) or (MACOS and ('macos' in self.os or 'unix' in self.os))):
1971 return True
1972 else:
1973 return False
1974 else:
1975 if not hasattr(self, 'macos_url') and not hasattr(self, 'windows_url') and not hasattr(self, 'unix_url') and not hasattr(self, 'linux_url'):
1976 return True
1977
1978 if MACOS and hasattr(self, 'macos_url') and self.compatible_with_this_arch():
1979 return True
1980
1981 if LINUX and hasattr(self, 'linux_url') and self.compatible_with_this_arch():
1982 return True
1983
1984 if WINDOWS and hasattr(self, 'windows_url') and self.compatible_with_this_arch():
1985 return True
1986
1987 if UNIX and hasattr(self, 'unix_url'):
1988 return True
1989
1990 return hasattr(self, 'url')
1991
1992 # the "version file" is a file inside install dirs that indicates the
1993 # version installed there. this helps disambiguate when there is more than
1994 # one version that may be installed to the same directory (which is used
1995 # to avoid accumulating builds over time in some cases, with new builds
1996 # overwriting the old)
1997 def get_version_file_path(self):
1998 return os.path.join(self.installation_path(), '.emsdk_version')
1999
2000 def is_installed_version(self):
2001 version_file_path = self.get_version_file_path()
2002 if os.path.isfile(version_file_path):
2003 with open(version_file_path) as version_file:
2004 return version_file.read().strip() == self.name
2005 return False
2006
2007 def update_installed_version(self):
2008 with open(self.get_version_file_path(), 'w') as version_file:
2009 version_file.write(self.name + '\n')
2010
2011 def is_installed(self, skip_version_check=False):
2012 # If this tool/sdk depends on other tools, require that all dependencies are
2013 # installed for this tool to count as being installed.
2014 if hasattr(self, 'uses'):
2015 for tool_name in self.uses:
2016 tool = find_tool(tool_name)
2017 if tool is None:
2018 errlog(f"Manifest error: No tool by name '{tool_name}' found! This may indicate an internal SDK error!")
2019 return False
2020 if not tool.is_installed():
2021 return False
2022
2023 if self.download_url() is None:
2024 debug_print(str(self) + ' has no files to download, so is installed by default.')
2025 return True
2026
2027 content_exists = is_nonempty_directory(self.installation_path())
2028 debug_print(str(self) + ' installation path is ' + self.installation_path() + ', exists: ' + str(content_exists) + '.')
2029
2030 # For e.g. fastcomp clang from git repo, the activated PATH is the
2031 # directory where the compiler is built to, and installation_path is
2032 # the directory where the source tree exists. To distinguish between
2033 # multiple packages sharing the same source (clang-main-32bit,
2034 # clang-main-64bit, clang-main-32bit and clang-main-64bit each
2035 # share the same git repo), require that in addition to the installation
2036 # directory, each item in the activated PATH must exist.
2037 if hasattr(self, 'activated_path') and not os.path.exists(self.expand_vars(self.activated_path)):
2038 content_exists = False
2039
2040 if hasattr(self, 'custom_is_installed_script'):
2041 if self.custom_is_installed_script == 'is_binaryen_installed':
2042 return is_binaryen_installed(self)
2043 elif self.custom_is_installed_script == 'is_firefox_installed':
2044 return is_firefox_installed(self)
2045 else:
2046 raise Exception('Unknown custom_is_installed_script directive "' + self.custom_is_installed_script + '"!')
2047
2048 return content_exists and (skip_version_check or self.is_installed_version())
2049
2050 def is_active(self):
2051 if not self.is_installed():
2052 return False
2053
2054 # All dependencies of this tool must be active as well.
2055 deps = self.dependencies()
2056 for tool in deps:
2057 if not tool.is_active():
2058 return False
2059
2060 activated_cfg = self.activated_config()
2061 if not activated_cfg:
2062 return len(deps) > 0
2063
2064 for key, value in activated_cfg.items():
2065 if key not in EM_CONFIG_DICT:
2066 debug_print(f'{self} is not active, because key="{key}" does not exist in .emscripten')
2067 return False
2068
2069 # all paths are stored dynamically relative to the emsdk root, so
2070 # normalize those first.
2071 config_value = EM_CONFIG_DICT[key].replace("emsdk_path + '", "'" + EMSDK_PATH)
2072 config_value = config_value.strip("'")
2073 if config_value != value:
2074 debug_print(f'{self} is not active, because key="{key}" has value "{config_value}" but should have value "{value}"')
2075 return False
2076 return True
2077
2078 # Returns true if the system environment variables requires by this tool are currently active.
2079 def is_env_active(self):
2080 envs = self.activated_environment()
2081 for env in envs:
2082 key, value = parse_key_value(env)
2083 if key not in os.environ or to_unix_path(os.environ[key]) != to_unix_path(value):
2084 debug_print(f'{self} is not active, because environment variable key="{key}" has value "{os.getenv(key)}" but should have value "{value}"')
2085 return False
2086
2087 if hasattr(self, 'activated_path'):
2088 path = to_unix_path(self.expand_vars(self.activated_path))
2089 for p in path:
2090 path_items = os.environ['PATH'].replace('\\', '/').split(ENVPATH_SEPARATOR)
2091 if not normalized_contains(path_items, p):
2092 debug_print(f'{self} is not active, because environment variable PATH item "{p}" is not present (PATH={os.environ["PATH"]})')
2093 return False
2094 return True
2095
2096 # If this tool can be installed on this system, this function returns True.
2097 # Otherwise, this function returns a string that describes the reason why this
2098 # tool is not available.
2099 def can_be_installed(self):
2100 if hasattr(self, 'bitness'):
2101 if self.bitness == 64 and not is_os_64bit():
2102 return "this tool is only provided for 64-bit OSes"
2103 return True
2104
2105 def download_url(self):
2106 if WINDOWS and hasattr(self, 'windows_url'):
2107 return self.windows_url
2108 elif MACOS and hasattr(self, 'macos_url'):
2109 return self.macos_url
2110 elif LINUX and hasattr(self, 'linux_url'):
2111 return self.linux_url
2112 elif UNIX and hasattr(self, 'unix_url'):
2113 return self.unix_url
2114 elif hasattr(self, 'url'):
2115 return self.url
2116 else:
2117 return None
2118
2119 def install(self):
2120 """Returns True if the Tool was installed of False if was skipped due to
2121 already being installed.
2122 """
2123 if self.can_be_installed() is not True:
2124 exit_with_error(f"The tool '{self}' is not available due to the reason: {self.can_be_installed()}")
2125
2126 if self.id == 'sdk':
2127 return self.install_sdk()
2128 else:
2129 return self.install_tool()
2130
2131 def install_sdk(self):
2132 """Returns True if any SDK component was installed of False all componented
2133 were already installed.
2134 """
2135 print(f"Installing SDK '{self}'..")
2136 installed = False
2137
2138 for tool_name in self.uses:
2139 tool = find_tool(tool_name)
2140 if tool is None:
2141 exit_with_error(f"manifest error: No tool by name '{tool_name}' found! This may indicate an internal SDK error!")
2142 installed |= tool.install()
2143
2144 if not installed:
2145 print(f"All SDK components already installed: '{self}'.")
2146 return False
2147
2148 if getattr(self, 'custom_install_script', None) == 'emscripten_npm_install':
2149 # upstream tools have hardcoded paths that are not stored in emsdk_manifest.json registry
2150 install_path = 'upstream'
2151 emscripten_dir = os.path.join(EMSDK_PATH, install_path, 'emscripten')
2152 # Older versions of the sdk did not include the node_modules directory
2153 # and require `npm ci` to be run post-install
2154 if not os.path.exists(os.path.join(emscripten_dir, 'node_modules')):
2155 if not emscripten_npm_install(self, emscripten_dir):
2156 exit_with_error('post-install step failed: emscripten_npm_install')
2157
2158 print(f"Done installing SDK '{self}'.")
2159 return True
2160
2161 def install_tool(self):
2162 """Returns True if the SDK was installed of False if was skipped due to
2163 already being installed.
2164 """
2165 # Avoid doing a redundant reinstall of the tool, if it has already been installed.
2166 # However all tools that are sourced directly from git branches do need to be
2167 # installed every time when requested, since the install step is then used to git
2168 # pull the tool to a newer version.
2169 if self.is_installed() and not hasattr(self, 'git_branch'):
2170 print(f"Skipped installing {self.name}, already installed.")
2171 return False
2172
2173 print(f"Installing tool '{self}'..")
2174 url = self.download_url()
2175
2176 custom_install_scripts = {
2177 'build_llvm': build_llvm,
2178 'build_ninja': build_ninja,
2179 'build_ccache': build_ccache,
2180 'download_node_nightly': download_node_nightly,
2181 'download_firefox': download_firefox,
2182 }
2183 if hasattr(self, 'custom_install_script') and self.custom_install_script in custom_install_scripts:
2184 success = custom_install_scripts[self.custom_install_script](self)
2185 elif hasattr(self, 'git_branch'):
2186 success = git_clone_checkout_and_pull(url, self.installation_path(), self.git_branch, getattr(self, 'remote_name', 'origin'))
2187 elif url.endswith(ARCHIVE_SUFFIXES):
2188 success = download_and_extract(url, self.installation_path(),
2189 filename_prefix=getattr(self, 'download_prefix', ''))
2190 else:
2191 assert False, 'unhandled url type: ' + url
2192
2193 if not success:
2194 exit_with_error("installation failed!")
2195
2196 if hasattr(self, 'custom_install_script'):
2197 if self.custom_install_script == 'emscripten_npm_install':
2198 success = emscripten_npm_install(self, self.installation_path())
2199 elif self.custom_install_script in ('build_llvm', 'build_ninja', 'build_ccache', 'download_node_nightly', 'download_firefox'):
2200 # 'build_llvm' is a special one that does the download on its
2201 # own, others do the download manually.
2202 pass
2203 elif self.custom_install_script == 'build_binaryen':
2204 success = build_binaryen_tool(self)
2205 else:
2206 raise Exception('Unknown custom_install_script command "' + self.custom_install_script + '"!')
2207
2208 if not success:
2209 exit_with_error("installation failed!")
2210
2211 # Install an emscripten-version.txt file if told to, and if there is one.
2212 # (If this is not an actual release, but some other build, then we do not
2213 # write anything.)
2214 if hasattr(self, 'emscripten_releases_hash'):
2215 emscripten_version_file_path = os.path.join(to_native_path(self.expand_vars(self.activated_path)), 'emscripten-version.txt')
2216 version = get_emscripten_release_version(self.emscripten_releases_hash)
2217 if version:
2218 with open(emscripten_version_file_path, 'w') as f:
2219 f.write('"%s"\n' % version)
2220
2221 print(f"Done installing tool '{self}'.")
2222
2223 # Sanity check that the installation succeeded, and if so, remove unneeded
2224 # leftover installation files.
2225 if not self.is_installed(skip_version_check=True):
2226 exit_with_error(f"installation of '{self}' failed, but no error was detected. Either something went wrong with the installation, or this may indicate an internal emsdk error.")
2227
2228 self.cleanup_temp_install_files()
2229 self.update_installed_version()
2230 return True
2231
2232 def cleanup_temp_install_files(self):
2233 if KEEP_DOWNLOADS:
2234 return
2235 url = self.download_url()
2236 if url.endswith(ARCHIVE_SUFFIXES):
2237 download_target = get_download_target(url, download_dir, getattr(self, 'download_prefix', ''))
2238 debug_print(f"Deleting temporary download: {download_target}")
2239 rmfile(download_target)
2240
2241 def uninstall(self):
2242 if not self.is_installed():
2243 print(f"Tool '{self}' was not installed. No need to uninstall.")
2244 return
2245 print(f"Uninstalling tool '{self}'..")
2246 if hasattr(self, 'custom_uninstall_script'):
2247 if self.custom_uninstall_script == 'uninstall_binaryen':
2248 uninstall_binaryen(self)
2249 else:
2250 raise Exception(f'Unknown custom_uninstall_script directive "{self.custom_uninstall_script}"!')
2251 print(f"Deleting path '{self.installation_path()}'")
2252 remove_tree(self.installation_path())
2253 print(f"Done uninstalling '{self}'.")
2254
2255 def dependencies(self):
2256 if not hasattr(self, 'uses'):
2257 return []
2258 deps = []
2259
2260 for tool_name in self.uses:
2261 tool = find_tool(tool_name)
2262 if tool:
2263 deps += [tool]
2264 return deps
2265
2266 def recursive_dependencies(self):
2267 if not hasattr(self, 'uses'):
2268 return []
2269 deps = []
2270 for tool_name in self.uses:
2271 tool = find_tool(tool_name)
2272 if tool:
2273 deps += [tool]
2274 deps += tool.recursive_dependencies()
2275 return deps
2276
2277
2278 # A global registry of all known Emscripten SDK tools available in the SDK manifest.
2279 tools = []
2280 tools_map = {}
2281
2282
2283 def add_tool(tool):
2284 tool.is_sdk = False
2285 tools.append(tool)
2286 if find_tool(str(tool)):
2287 raise Exception('Duplicate tool ' + str(tool) + '! Existing:\n{' + ', '.join("%s: %s" % item for item in vars(find_tool(str(tool))).items()) + '}, New:\n{' + ', '.join("%s: %s" % item for item in vars(tool).items()) + '}')
2288 tools_map[str(tool)] = tool
2289
2290
2291 # A global registry of all known SDK toolsets.
2292 sdks = []
2293 sdks_map = {}
2294
2295
2296 def add_sdk(sdk):
2297 sdk.is_sdk = True
2298 sdks.append(sdk)
2299 if find_sdk(str(sdk)):
2300 raise Exception('Duplicate sdk ' + str(sdk) + '! Existing:\n{' + ', '.join("%s: %s" % item for item in vars(find_sdk(str(sdk))).items()) + '}, New:\n{' + ', '.join("%s: %s" % item for item in vars(sdk).items()) + '}')
2301 sdks_map[str(sdk)] = sdk
2302
2303
2304 # N.B. In both tools and sdks list above, we take the convention that the newest
2305 # items are at the back of the list (ascending chronological order)
2306
2307 def find_tool(name):
2308 return tools_map.get(name)
2309
2310
2311 def find_sdk(name):
2312 return sdks_map.get(name)
2313
2314
2315 def is_os_64bit():
2316 return ARCH.endswith('64')
2317
2318
2319 def find_latest_version():
2320 return resolve_sdk_aliases('latest')
2321
2322
2323 def find_latest_hash():
2324 version = find_latest_version()
2325 releases_info = load_releases_info()
2326 return releases_info['releases'][version]
2327
2328
2329 def resolve_sdk_aliases(name, verbose=False):
2330 releases_info = load_releases_info()
2331 while name in releases_info['aliases']:
2332 if verbose:
2333 print("Resolving SDK alias '%s' to '%s'" % (name, releases_info['aliases'][name]))
2334 name = releases_info['aliases'][name]
2335 return name
2336
2337
2338 def find_latest_sdk():
2339 return 'sdk-releases-%s-64bit' % (find_latest_hash())
2340
2341
2342 def find_tot_sdk():
2343 debug_print('Fetching emscripten-releases repository...')
2344 global extra_release_tag
2345 extra_release_tag = get_emscripten_releases_tot()
2346 return 'sdk-releases-%s-64bit' % (extra_release_tag)
2347
2348
2349 def parse_emscripten_version(emscripten_root):
2350 version_file = os.path.join(emscripten_root, 'emscripten-version.txt')
2351 with open(version_file) as f:
2352 version = f.read().strip()
2353 version = version.strip('"').split('-')[0].split('.')
2354 return [int(v) for v in version]
2355
2356
2357 # Given a git hash in emscripten-releases, find the emscripten
2358 # version for it. There may not be one if this is not the hash of
2359 # a release, in which case we return None.
2360 def get_emscripten_release_version(emscripten_releases_hash):
2361 releases_info = load_releases_info()
2362 for key, value in dict(releases_info['releases']).items():
2363 if value == emscripten_releases_hash:
2364 return key.split('-')[0]
2365 return None
2366
2367
2368 # Get the tip-of-tree build identifier.
2369 def get_emscripten_releases_tot():
2370 git_clone_checkout_and_pull(emscripten_releases_repo, sdk_path('releases'), 'main')
2371 recent_releases = git_recent_commits(sdk_path('releases'))
2372 # The recent releases are the latest hashes in the git repo. There
2373 # may not be a build for the most recent ones yet; find the last
2374 # that does.
2375 arch = ''
2376 if ARCH == 'arm64':
2377 arch = '-arm64'
2378
2379 def make_url(ext):
2380 return emscripten_releases_download_url_template % (
2381 os_name(),
2382 release,
2383 arch,
2384 ext,
2385 )
2386
2387 for release in recent_releases:
2388 make_url('tar.xz' if not WINDOWS else 'zip')
2389 try:
2390 urlopen(make_url('tar.xz' if not WINDOWS else 'zip'))
2391 except Exception:
2392 if not WINDOWS:
2393 # Try the old `.tbz2` name
2394 # TODO:remove this once tot builds are all using xz
2395 try:
2396 urlopen(make_url('tbz2'))
2397 except Exception:
2398 continue
2399 else:
2400 continue
2401 return release
2402 exit_with_error('failed to find build of any recent emsdk revision')
2403
2404
2405 def get_release_hash(arg, releases_info):
2406 return releases_info.get(arg, None) or releases_info.get(f'sdk-{arg}-64bit')
2407
2408
2409 def version_key(ver):
2410 return tuple(map(int, re.split('[._-]', ver)[:3]))
2411
2412
2413 def is_emsdk_sourced_from_github():
2414 return os.path.exists(os.path.join(EMSDK_PATH, '.git'))
2415
2416
2417 def update_emsdk():
2418 if is_emsdk_sourced_from_github():
2419 errlog('You seem to have bootstrapped Emscripten SDK by cloning from GitHub. In this case, use "git pull" instead of "emsdk update" to update emsdk. (Not doing that automatically in case you have local changes)')
2420 sys.exit(1)
2421 if not download_and_extract(emsdk_zip_download_url, EMSDK_PATH, clobber=False):
2422 sys.exit(1)
2423
2424
2425 # Lists all legacy (pre-emscripten-releases) tagged versions directly in the Git
2426 # repositories. These we can pull and compile from source.
2427 def load_legacy_emscripten_tags():
2428 return open(sdk_path('legacy-emscripten-tags.txt')).read().split('\n')
2429
2430
2431 def load_legacy_binaryen_tags():
2432 return open(sdk_path('legacy-binaryen-tags.txt')).read().split('\n')
2433
2434
2435 def remove_prefix(s, prefix):
2436 if s.startswith(prefix):
2437 return s[len(prefix):]
2438 else:
2439 return s
2440
2441
2442 def remove_suffix(s, suffix):
2443 if s.endswith(suffix):
2444 return s[:len(s) - len(suffix)]
2445 else:
2446 return s
2447
2448
2449 # filename should be one of: 'llvm-precompiled-tags-32bit.txt', 'llvm-precompiled-tags-64bit.txt'
2450 def load_file_index_list(filename):
2451 items = open(sdk_path(filename)).read().splitlines()
2452 items = [remove_suffix(remove_suffix(remove_prefix(x, 'emscripten-llvm-e'), '.tar.gz'), '.zip').strip() for x in items]
2453 items = [x for x in items if 'latest' not in x and len(x) > 0]
2454
2455 # Sort versions from oldest to newest (the default sort would be
2456 # lexicographic, i.e. '1.37.1 < 1.37.10 < 1.37.2')
2457 return sorted(items, key=version_key)
2458
2459
2460 # Load the json info for emscripten-releases.
2461 def load_releases_info():
2462 if not hasattr(load_releases_info, 'cached_info'):
2463 try:
2464 text = open(sdk_path('emscripten-releases-tags.json')).read()
2465 load_releases_info.cached_info = json.loads(text)
2466 except Exception as e:
2467 print('Error parsing emscripten-releases-tags.json!')
2468 exit_with_error(str(e))
2469
2470 return load_releases_info.cached_info
2471
2472
2473 def get_installed_sdk_version():
2474 version_file = sdk_path(os.path.join('upstream', '.emsdk_version'))
2475 if not os.path.exists(version_file):
2476 return None
2477 with open(version_file) as f:
2478 version = f.read()
2479 return version.split('-')[1]
2480
2481
2482 # Get a list of tags for emscripten-releases.
2483 def load_releases_tags():
2484 tags = []
2485 info = load_releases_info()
2486
2487 for _version, sha in sorted(info['releases'].items(), key=lambda x: version_key(x[0])):
2488 tags.append(sha)
2489
2490 if extra_release_tag:
2491 tags.append(extra_release_tag)
2492
2493 # Explicitly add the currently installed SDK version. This could be a custom
2494 # version (installed explicitly) so it might not be part of the main list
2495 # loaded above.
2496 installed = get_installed_sdk_version()
2497 if installed and installed not in tags:
2498 tags.append(installed)
2499
2500 return tags
2501
2502
2503 def load_releases_versions():
2504 info = load_releases_info()
2505 versions = list(info['releases'].keys())
2506 return versions
2507
2508
2509 def load_sdk_manifest():
2510 try:
2511 manifest = json.loads(open(sdk_path("emsdk_manifest.json")).read())
2512 except Exception as e:
2513 print('Error parsing emsdk_manifest.json!')
2514 print(str(e))
2515 return
2516
2517 emscripten_tags = load_legacy_emscripten_tags()
2518 llvm_precompiled_tags_32bit = []
2519 llvm_precompiled_tags_64bit = load_file_index_list('llvm-tags-64bit.txt')
2520 llvm_precompiled_tags = llvm_precompiled_tags_32bit + llvm_precompiled_tags_64bit
2521 binaryen_tags = load_legacy_binaryen_tags()
2522 releases_tags = load_releases_tags()
2523
2524 def dependencies_exist(sdk):
2525 for tool_name in sdk.uses:
2526 tool = find_tool(tool_name)
2527 if not tool:
2528 debug_print('missing dependency: ' + tool_name)
2529 return False
2530 return True
2531
2532 def cmp_version(ver, cmp_operand, reference):
2533 if cmp_operand == '<=':
2534 return version_key(ver) <= version_key(reference)
2535 if cmp_operand == '<':
2536 return version_key(ver) < version_key(reference)
2537 if cmp_operand == '>=':
2538 return version_key(ver) >= version_key(reference)
2539 if cmp_operand == '>':
2540 return version_key(ver) > version_key(reference)
2541 if cmp_operand == '==':
2542 return version_key(ver) == version_key(reference)
2543 if cmp_operand == '!=':
2544 return version_key(ver) != version_key(reference)
2545 raise Exception(f'Invalid cmp_operand "{cmp_operand}"!')
2546
2547 def passes_filters(param, ver, filters):
2548 for v in filters:
2549 if v[0] == param and not cmp_version(ver, v[1], v[2]):
2550 return False
2551 return True
2552
2553 # A 'category parameter' is a %foo%-encoded identifier that specifies
2554 # a class of tools instead of just one tool, e.g. %tag%
2555 def expand_category_param(param, category_list, t, is_sdk):
2556 for i, ver in enumerate(category_list):
2557 if not ver.strip():
2558 continue
2559 t2 = copy.copy(t)
2560 found_param = False
2561 for p, v in vars(t2).items():
2562 if isinstance(v, str) and param in v:
2563 t2.__dict__[p] = v.replace(param, ver)
2564 found_param = True
2565 if not found_param:
2566 continue
2567 t2.is_old = i < len(category_list) - 2
2568 if hasattr(t2, 'uses'):
2569 t2.uses = [x.replace(param, ver) for x in t2.uses]
2570
2571 # Filter out expanded tools by version requirements, such as ["tag", "<=", "1.37.22"]
2572 if hasattr(t2, 'version_filter'):
2573 passes = passes_filters(param, ver, t2.version_filter)
2574 if not passes:
2575 continue
2576
2577 if is_sdk:
2578 if dependencies_exist(t2):
2579 if not find_sdk(t2.name):
2580 add_sdk(t2)
2581 else:
2582 debug_print('SDK ' + str(t2) + ' already existed in manifest, not adding twice')
2583 else:
2584 if not find_tool(t2.name):
2585 add_tool(t2)
2586 else:
2587 debug_print('Tool ' + str(t2) + ' already existed in manifest, not adding twice')
2588
2589 for tool in manifest['tools']:
2590 t = Tool(tool)
2591 if t.compatible_with_this_os():
2592 if not hasattr(t, 'is_old'):
2593 t.is_old = False
2594
2595 # Expand the metapackages that refer to tags
2596 if '%tag%' in t.version:
2597 expand_category_param('%tag%', emscripten_tags, t, is_sdk=False)
2598 elif '%precompiled_tag%' in t.version:
2599 expand_category_param('%precompiled_tag%', llvm_precompiled_tags, t, is_sdk=False)
2600 elif '%precompiled_tag32%' in t.version:
2601 expand_category_param('%precompiled_tag32%', llvm_precompiled_tags_32bit, t, is_sdk=False)
2602 elif '%precompiled_tag64%' in t.version:
2603 expand_category_param('%precompiled_tag64%', llvm_precompiled_tags_64bit, t, is_sdk=False)
2604 elif '%binaryen_tag%' in t.version:
2605 expand_category_param('%binaryen_tag%', binaryen_tags, t, is_sdk=False)
2606 elif '%releases-tag%' in t.version:
2607 expand_category_param('%releases-tag%', releases_tags, t, is_sdk=False)
2608 else:
2609 add_tool(t)
2610
2611 for sdk_str in manifest['sdks']:
2612 sdk_str['id'] = 'sdk'
2613 sdk = Tool(sdk_str)
2614 if sdk.compatible_with_this_os():
2615 if not hasattr(sdk, 'is_old'):
2616 sdk.is_old = False
2617
2618 if '%tag%' in sdk.version:
2619 expand_category_param('%tag%', emscripten_tags, sdk, is_sdk=True)
2620 elif '%precompiled_tag%' in sdk.version:
2621 expand_category_param('%precompiled_tag%', llvm_precompiled_tags, sdk, is_sdk=True)
2622 elif '%precompiled_tag32%' in sdk.version:
2623 expand_category_param('%precompiled_tag32%', llvm_precompiled_tags_32bit, sdk, is_sdk=True)
2624 elif '%precompiled_tag64%' in sdk.version:
2625 expand_category_param('%precompiled_tag64%', llvm_precompiled_tags_64bit, sdk, is_sdk=True)
2626 elif '%releases-tag%' in sdk.version:
2627 expand_category_param('%releases-tag%', releases_tags, sdk, is_sdk=True)
2628 else:
2629 add_sdk(sdk)
2630
2631
2632 # Tests if the two given tools can be active at the same time.
2633 # Currently only a simple check for name for same tool with different versions,
2634 # possibly adds more logic in the future.
2635 def can_simultaneously_activate(tool1, tool2):
2636 return tool1.id != tool2.id
2637
2638
2639 # Expands dependencies for each tool, and removes ones that don't exist.
2640 def process_tool_list(tools_to_activate):
2641 i = 0
2642 # Gather dependencies for each tool
2643 while i < len(tools_to_activate):
2644 tool = tools_to_activate[i]
2645 deps = tool.recursive_dependencies()
2646 tools_to_activate = tools_to_activate[:i] + deps + tools_to_activate[i:]
2647 i += len(deps) + 1
2648
2649 for tool in tools_to_activate:
2650 if not tool.is_installed():
2651 exit_with_error("error: tool is not installed and therefore cannot be activated: '%s'" % tool)
2652
2653 # Remove conflicting tools
2654 i = 0
2655 while i < len(tools_to_activate):
2656 j = 0
2657 while j < i:
2658 secondary_tool = tools_to_activate[j]
2659 primary_tool = tools_to_activate[i]
2660 if not can_simultaneously_activate(primary_tool, secondary_tool):
2661 tools_to_activate.pop(j)
2662 j -= 1
2663 i -= 1
2664 j += 1
2665 i += 1
2666 return tools_to_activate
2667
2668
2669 def write_set_env_script(env_string):
2670 assert CMD or POWERSHELL
2671 open(EMSDK_SET_ENV, 'w').write(env_string)
2672
2673
2674 # Reconfigure .emscripten to choose the currently activated toolset, set PATH
2675 # and other environment variables.
2676 # Returns the full list of deduced tools that are now active.
2677 def set_active_tools(tools_to_activate, permanently_activate, system):
2678 tools_to_activate = process_tool_list(tools_to_activate)
2679
2680 if tools_to_activate:
2681 tools = [x for x in tools_to_activate if not x.is_sdk]
2682 print('Setting the following tools as active:\n ' + '\n '.join([str(t) for t in tools]))
2683 print('')
2684
2685 generate_em_config(tools_to_activate, permanently_activate, system)
2686
2687 # Construct a .bat or .ps1 script that will be invoked to set env. vars and PATH
2688 # We only do this on cmd or powershell since emsdk.bat/ps1 is able to modify the
2689 # calling shell environment. On other shell `source emsdk_env.sh` is
2690 # required.
2691 if CMD or POWERSHELL:
2692 # always set local environment variables since permanently activating will only set the registry settings and
2693 # will not affect the current session
2694 env_vars_to_add = get_env_vars_to_add(tools_to_activate, system, user=permanently_activate)
2695 env_string = construct_env_with_vars(env_vars_to_add)
2696 write_set_env_script(env_string)
2697
2698 if WINDOWS and permanently_activate:
2699 win_set_environment_variables(env_vars_to_add, system, user=permanently_activate)
2700
2701 return tools_to_activate
2702
2703
2704 def currently_active_sdk():
2705 for sdk in reversed(sdks):
2706 if sdk.is_active():
2707 return sdk
2708 return None
2709
2710
2711 def currently_active_tools():
2712 active_tools = []
2713 for tool in tools:
2714 if tool.is_active():
2715 active_tools += [tool]
2716 return active_tools
2717
2718
2719 # http://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-in-python-whilst-preserving-order
2720 def unique_items(seq):
2721 seen = set()
2722 seen_add = seen.add
2723 return [x for x in seq if x not in seen and not seen_add(x)]
2724
2725
2726 # Tests if a path is contained in the given list, but with separators normalized.
2727 def normalized_contains(lst, elem):
2728 elem = to_unix_path(elem)
2729 for e in lst:
2730 if elem == to_unix_path(e):
2731 return True
2732 return False
2733
2734
2735 def to_msys_path(p):
2736 p = to_unix_path(p)
2737 new_path = re.sub(r'([a-zA-Z]):/(.*)', r'/\1/\2', p)
2738 if len(new_path) > 3 and new_path[0] == '/' and new_path[2] == '/':
2739 new_path = new_path[0] + new_path[1].lower() + new_path[2:]
2740 return new_path
2741
2742
2743 # Looks at the current PATH and adds and removes entries so that the PATH reflects
2744 # the set of given active tools.
2745 def adjusted_path(tools_to_activate, system=False, user=False):
2746 # These directories should be added to PATH
2747 path_add = get_required_path(tools_to_activate)
2748 # These already exist.
2749 if WINDOWS and not MSYS:
2750 existing_path = win_get_environment_variable('PATH', system=system, user=user, fallback=True).split(ENVPATH_SEPARATOR)
2751 else:
2752 existing_path = os.environ['PATH'].split(ENVPATH_SEPARATOR)
2753
2754 existing_emsdk_tools = []
2755 existing_nonemsdk_path = []
2756 for entry in existing_path:
2757 if to_unix_path(entry).startswith(EMSDK_PATH):
2758 existing_emsdk_tools.append(entry)
2759 else:
2760 existing_nonemsdk_path.append(entry)
2761
2762 new_emsdk_tools = []
2763 kept_emsdk_tools = []
2764 for entry in path_add:
2765 if not normalized_contains(existing_emsdk_tools, entry):
2766 new_emsdk_tools.append(entry)
2767 else:
2768 kept_emsdk_tools.append(entry)
2769
2770 whole_path = unique_items(new_emsdk_tools + kept_emsdk_tools + existing_nonemsdk_path)
2771
2772 if MSYS:
2773 # XXX Hack: If running native Windows Python in MSYS prompt where PATH
2774 # entries look like "/c/Windows/System32", os.environ['PATH']
2775 # in Python will transform to show them as "C:\\Windows\\System32", so need
2776 # to reconvert path delimiter back to forward slashes.
2777 whole_path = [to_msys_path(p) for p in whole_path]
2778 new_emsdk_tools = [to_msys_path(p) for p in new_emsdk_tools]
2779
2780 separator = ':' if MSYS else ENVPATH_SEPARATOR
2781 return (separator.join(whole_path), new_emsdk_tools)
2782
2783
2784 def get_env_vars_to_add(tools_to_activate, system, user):
2785 env_vars_to_add = []
2786
2787 newpath, added_path = adjusted_path(tools_to_activate, system, user)
2788
2789 # Don't bother setting the path if there are no changes.
2790 if os.environ['PATH'] != newpath:
2791 env_vars_to_add += [('PATH', newpath)]
2792
2793 if added_path:
2794 info('Adding directories to PATH:')
2795 for item in added_path:
2796 info('PATH += ' + item)
2797 info('')
2798
2799 # A core variable EMSDK points to the root of Emscripten SDK directory.
2800 env_vars_to_add += [('EMSDK', EMSDK_PATH)]
2801
2802 for tool in tools_to_activate:
2803 for env in tool.activated_environment():
2804 key, value = parse_key_value(env)
2805 value = to_native_path(tool.expand_vars(value))
2806 env_vars_to_add += [(key, value)]
2807
2808 emroot = find_emscripten_root(tools_to_activate)
2809 if emroot:
2810 # For older emscripten versions that don't use an embedded cache by
2811 # default we need to export EM_CACHE.
2812 #
2813 # Sadly, we can't put this in the config file since those older versions
2814 # also didn't read the `CACHE` key from the config file:
2815 #
2816 # History:
2817 # - 'CACHE' config started being honored in 1.39.16
2818 # https://github.com/emscripten-core/emscripten/pull/11091
2819 # - Default to embedded cache also started in 1.39.16
2820 # https://github.com/emscripten-core/emscripten/pull/11126
2821 # - Emscripten supports automatically locating the embedded
2822 # config in 1.39.13:
2823 # https://github.com/emscripten-core/emscripten/pull/10935
2824 #
2825 # Since setting EM_CACHE in the environment effects the entire machine
2826 # we want to avoid this except when installing these older emscripten
2827 # versions that really need it.
2828 version = parse_emscripten_version(emroot)
2829 if version < [1, 39, 16]:
2830 em_cache_dir = os.path.join(emroot, 'cache')
2831 env_vars_to_add += [('EM_CACHE', em_cache_dir)]
2832 if version < [1, 39, 13]:
2833 env_vars_to_add += [('EM_CONFIG', os.path.normpath(EM_CONFIG_PATH))]
2834
2835 return env_vars_to_add
2836
2837
2838 def construct_env(tools_to_activate, system, user):
2839 info('Setting up EMSDK environment (suppress these messages with EMSDK_QUIET=1)')
2840 return construct_env_with_vars(get_env_vars_to_add(tools_to_activate, system, user))
2841
2842
2843 def unset_env(key):
2844 if POWERSHELL:
2845 return 'Remove-Item env:%s\n' % key
2846 if CMD:
2847 return 'set %s=\n' % key
2848 if CSH:
2849 return 'unsetenv %s;\n' % key
2850 if FISH:
2851 return 'set -e %s;\n' % key
2852 if BASH:
2853 return 'unset %s;\n' % key
2854 assert False
2855
2856
2857 def construct_env_with_vars(env_vars_to_add):
2858 env_string = ''
2859 if env_vars_to_add:
2860 info('Setting environment variables:')
2861
2862 for key, value in env_vars_to_add:
2863 # Don't set env vars which are already set to the correct value.
2864 if key in os.environ and to_unix_path(os.environ[key]) == to_unix_path(value):
2865 continue
2866 info(key + ' = ' + value)
2867 if POWERSHELL:
2868 env_string += f'$env:{key}="{value}"\n'
2869 elif CMD:
2870 env_string += f'SET {key}={value}\n'
2871 elif CSH:
2872 env_string += f'setenv {key} "{value}";\n'
2873 elif FISH:
2874 env_string += f'set -gx {key} "{value}";\n'
2875 elif BASH:
2876 env_string += f'export {key}="{value}";\n'
2877 else:
2878 assert False
2879
2880 if 'EMSDK_PYTHON' in env_vars_to_add:
2881 # When using our bundled python we never want the user's
2882 # PYTHONHOME or PYTHONPATH
2883 # See https://github.com/emscripten-core/emsdk/issues/598
2884 env_string += unset_env('PYTHONHOME')
2885 env_string += unset_env('PYTHONPATH')
2886
2887 # Remove any environment variables that might have been set by old or
2888 # inactive tools/sdks. For example, we set EM_CACHE for older versions
2889 # of the SDK but we want to remove that from the current environment
2890 # if no such tool is active.
2891 # Ignore certain keys that are inputs to emsdk itself.
2892 ignore_keys = {'EMSDK_POWERSHELL', 'EMSDK_CSH', 'EMSDK_CMD', 'EMSDK_BASH', 'EMSDK_FISH',
2893 'EMSDK_NUM_CORES', 'EMSDK_NOTTY', 'EMSDK_KEEP_DOWNLOADS'}
2894 env_keys_to_add = {pair[0] for pair in env_vars_to_add}
2895 for key in os.environ:
2896 if key.startswith('EMSDK_') or key in ('EM_CACHE', 'EM_CONFIG'):
2897 if key not in env_keys_to_add and key not in ignore_keys:
2898 info('Clearing existing environment variable: %s' % key)
2899 env_string += unset_env(key)
2900
2901 return env_string
2902
2903
2904 def error_on_missing_tool(name):
2905 if name.endswith('-64bit') and not is_os_64bit():
2906 exit_with_error("'%s' is only provided for 64-bit OSes" % name)
2907 else:
2908 exit_with_error("tool or SDK not found: '%s'" % name)
2909
2910
2911 def expand_sdk_name(name, activating):
2912 if 'upstream-master' in name:
2913 errlog('upstream-master SDK has been renamed main')
2914 name = name.replace('upstream-master', 'main')
2915 if 'fastcomp' in name:
2916 exit_with_error('the fastcomp backend is no longer supported. Please use an older version of emsdk (for example 3.1.29) if you want to install the old fastcomp-based SDK')
2917 if name in ('tot', 'sdk-tot', 'tot-upstream'):
2918 if activating:
2919 # When we are activating a tot release, assume that the currently
2920 # installed SDK, if any, is the tot release we want to activate.
2921 # Without this `install tot && activate tot` will race with the builders
2922 # that are producing new builds.
2923 installed = get_installed_sdk_version()
2924 if installed:
2925 debug_print('activating currently installed SDK; not updating tot version')
2926 return 'sdk-releases-%s-64bit' % installed
2927 return find_tot_sdk()
2928
2929 if '-upstream' in name:
2930 name = name.replace('-upstream', '')
2931
2932 name = resolve_sdk_aliases(name, verbose=True)
2933
2934 # check if it's a release handled by an emscripten-releases version,
2935 # and if so use that by using the right hash. we support a few notations,
2936 # x.y.z
2937 # sdk-x.y.z-64bit
2938 # TODO: support short notation for old builds too?
2939 fullname = name
2940 version = fullname.replace('sdk-', '').replace('releases-', '').replace('-64bit', '').replace('tag-', '')
2941 sdk = 'sdk-' if not name.startswith('releases-') else ''
2942 releases_info = load_releases_info()['releases']
2943 release_hash = get_release_hash(version, releases_info)
2944 if release_hash:
2945 # Known release hash
2946 full_name = '%sreleases-%s-64bit' % (sdk, release_hash)
2947 print("Resolving SDK version '%s' to '%s'" % (version, full_name))
2948 return full_name
2949
2950 if len(version) == 40:
2951 global extra_release_tag
2952 extra_release_tag = version
2953 return '%sreleases-%s-64bit' % (sdk, version)
2954
2955 return name
2956
2957
2958 def main(args): # noqa: C901, PLR0911, PLR0912, PLR0915
2959 if not args:
2960 errlog("Missing command; Type 'emsdk help' to get a list of commands.")
2961 return 1
2962
2963 debug_print('emsdk.py running under `%s`' % sys.executable)
2964 cmd = args.pop(0)
2965
2966 if cmd in ('help', '--help', '-h'):
2967 print(' emsdk: Available commands:')
2968
2969 print('''
2970 emsdk list [--old] [--uses] - Lists all available SDKs and tools and their
2971 current installation status. With the --old
2972 parameter, also historical versions are
2973 shown. If --uses is passed, displays the
2974 composition of different SDK packages and
2975 dependencies.
2976
2977 emsdk update - Updates emsdk to the newest version. If you have
2978 bootstrapped emsdk via cloning directly from
2979 GitHub, call "git pull" instead to update emsdk.
2980
2981 emsdk install [options] <tool 1> <tool 2> <tool 3> ...
2982 - Downloads and installs given tools or SDKs.
2983 Options can contain:
2984
2985 -j<num>: Specifies the number of cores to use when
2986 building the tool. Default: use one less
2987 than the # of detected cores.
2988
2989 --build=<type>: Controls what kind of build of LLVM to
2990 perform. Pass either 'Debug', 'Release',
2991 'MinSizeRel' or 'RelWithDebInfo'. Default:
2992 'Release'.
2993
2994 --generator=<type>: Specifies the CMake Generator to be used
2995 during the build. Possible values are the
2996 same as what your CMake supports and whether
2997 the generator is valid depends on the tools
2998 you have installed. Defaults to 'Unix Makefiles'
2999 on *nix systems. If generator name is multiple
3000 words, enclose with single or double quotes.
3001
3002 --shallow: When installing tools from one of the git
3003 development branches, this parameter can be
3004 passed to perform a shallow git clone instead
3005 of a full one. This reduces the amount of
3006 network transfer that is needed. This option
3007 should only be used when you are interested in
3008 downloading one of the development branches,
3009 but are not looking to develop Emscripten
3010 yourself. Default: disabled, i.e. do a full
3011 clone.
3012
3013 --build-tests: If enabled, LLVM is built with internal tests
3014 included. Pass this to enable running test
3015 other.test_llvm_lit in the Emscripten test
3016 suite. Default: disabled.
3017 --enable-assertions: If specified, LLVM is built with assert()
3018 checks enabled. Useful for development
3019 purposes. Default: Enabled
3020 --disable-assertions: Forces assertions off during the build.
3021
3022 --vs2019/--vs2022: If building from source, overrides to build
3023 using the specified compiler. When installing
3024 precompiled packages, this has no effect.
3025 Note: The same compiler specifier must be
3026 passed to the emsdk activate command to
3027 activate the desired version.
3028
3029 Notes on building from source:
3030
3031 To pass custom CMake directives when configuring
3032 LLVM build, specify the environment variable
3033 LLVM_CMAKE_ARGS="param1=value1,param2=value2"
3034 in the environment where the build is invoked.
3035 See README.md for details.
3036
3037 --override-repository: Specifies the git URL to use for a given Tool. E.g.
3038 --override-repository emscripten-main@https://github.com/<fork>/emscripten/tree/<refspec>
3039
3040
3041 emsdk deactivate tool/sdk - Removes the given tool or SDK from the current set of activated tools.
3042
3043
3044 emsdk uninstall <tool/sdk> - Removes the given tool or SDK from disk.''')
3045
3046 if WINDOWS:
3047 print('''
3048 emsdk activate [--permanent] [--system] [--build=type] [--vs2019/--vs2022] <tool/sdk>
3049
3050 - Activates the given tool or SDK in the
3051 environment of the current shell.
3052
3053 - If the `--permanent` option is passed, then the environment
3054 variables are set permanently for the current user.
3055
3056 - If the `--system` option is passed, the registration
3057 is done for all users of the system.
3058 This needs admin privileges
3059 (uses Machine environment variables).
3060
3061 - If a custom compiler version was used to override
3062 the compiler to use, pass the same --vs2019/--vs2022
3063 parameter here to choose which version to activate.
3064
3065 emcmdprompt.bat - Spawns a new command prompt window with the
3066 Emscripten environment active.''')
3067 else:
3068 print(''' emsdk activate [--build=type] <tool/sdk>
3069
3070 - Activates the given tool or SDK in the
3071 environment of the current shell.''')
3072
3073 print('''
3074 Both commands 'install' and 'activate' accept an optional parameter
3075 '--build=type', which can be used to override what kind of installation
3076 or activation to perform. Possible values for type are Debug, Release,
3077 MinSizeRel or RelWithDebInfo. Note: When overriding a custom build type,
3078 be sure to match the same --build= option to both 'install' and
3079 'activate' commands and the invocation of 'emsdk_env', or otherwise
3080 these commands will default to operating on the default build type
3081 which is RelWithDebInfo.''')
3082
3083 print('''
3084
3085 Environment:
3086 EMSDK_KEEP_DOWNLOADS=1 - if you want to keep the downloaded archives.
3087 EMSDK_NOTTY=1 - override isatty() result (mainly to log progress).
3088 EMSDK_NUM_CORES=n - limit parallelism to n cores.
3089 EMSDK_VERBOSE=1 - very verbose output, useful for debugging.''')
3090 return 0
3091
3092 # Extracts a boolean command line argument from args and returns True if it was present
3093 def extract_bool_arg(name):
3094 if name in args:
3095 args.remove(name)
3096 return True
3097 return False
3098
3099 def extract_string_arg(name):
3100 for i in range(len(args)):
3101 if args[i] == name:
3102 value = args[i + 1]
3103 del args[i:i + 2]
3104 return value
3105
3106 arg_old = extract_bool_arg('--old')
3107 arg_uses = extract_bool_arg('--uses')
3108 arg_permanent = extract_bool_arg('--permanent')
3109 arg_global = extract_bool_arg('--global')
3110 arg_system = extract_bool_arg('--system')
3111 if arg_global:
3112 print('--global is deprecated. Use `--system` to set the environment variables for all users')
3113 arg_system = True
3114 if arg_system:
3115 arg_permanent = True
3116 if extract_bool_arg('--embedded'):
3117 errlog('embedded mode is now the only mode available')
3118 if extract_bool_arg('--no-embedded'):
3119 errlog('embedded mode is now the only mode available')
3120 return 1
3121
3122 arg_notty = extract_bool_arg('--notty')
3123 if arg_notty:
3124 global TTY_OUTPUT
3125 TTY_OUTPUT = False
3126
3127 # Replace meta-packages with the real package names.
3128 if cmd in ('update', 'install', 'activate'):
3129 activating = cmd == 'activate'
3130 args = [expand_sdk_name(a, activating=activating) for a in args]
3131
3132 load_em_config()
3133 load_sdk_manifest()
3134
3135 # Apply any overrides to git branch names to clone from.
3136 forked_url = extract_string_arg('--override-repository')
3137 while forked_url:
3138 tool_name, url_and_refspec = forked_url.split('@')
3139 t = find_tool(tool_name)
3140 if not t:
3141 errlog('Failed to find tool {tool_name}!')
3142 return False
3143 else:
3144 t.url, t.git_branch, t.remote_name = parse_github_url_and_refspec(url_and_refspec)
3145 debug_print(f'Reading git repository URL "{t.url}" and git branch "{t.git_branch}" for Tool "{tool_name}".')
3146
3147 forked_url = extract_string_arg('--override-repository')
3148
3149 # Process global args
3150 for i in range(len(args)):
3151 if args[i].startswith('--generator='):
3152 build_generator = re.match(r'''^--generator=['"]?([^'"]+)['"]?$''', args[i])
3153 if build_generator:
3154 global CMAKE_GENERATOR
3155 CMAKE_GENERATOR = build_generator.group(1)
3156 args[i] = ''
3157 else:
3158 errlog(f"Cannot parse CMake generator string: {args[i]}. Try wrapping generator string with quotes")
3159 return 1
3160 elif args[i].startswith('--build='):
3161 build_type = re.match(r'^--build=(.+)$', args[i])
3162 if build_type:
3163 global CMAKE_BUILD_TYPE_OVERRIDE
3164 build_type = build_type.group(1)
3165 build_types = ['Debug', 'MinSizeRel', 'RelWithDebInfo', 'Release']
3166 try:
3167 build_type_index = [x.lower() for x in build_types].index(build_type.lower())
3168 CMAKE_BUILD_TYPE_OVERRIDE = build_types[build_type_index]
3169 args[i] = ''
3170 except Exception:
3171 errlog(f'Unknown CMake build type "{build_type}" specified! Please specify one of {build_types}')
3172 return 1
3173 else:
3174 errlog(f'Invalid command line parameter {args[i]} specified!')
3175 return 1
3176 args = [x for x in args if x]
3177
3178 if cmd == 'list':
3179 print('')
3180
3181 def installed_sdk_text(name):
3182 sdk = find_sdk(name)
3183 return 'INSTALLED' if sdk and sdk.is_installed() else ''
3184
3185 if (LINUX or MACOS or WINDOWS) and ARCH in ('x86', 'x86_64'):
3186 print('The *recommended* precompiled SDK download is %s (%s).' % (find_latest_version(), find_latest_hash()))
3187 print()
3188 print('To install/activate it use:')
3189 print(' latest')
3190 print('')
3191 print('This is equivalent to installing/activating:')
3192 print(' %s %s' % (find_latest_version(), installed_sdk_text(find_latest_sdk())))
3193 print('')
3194 else:
3195 print('Warning: your platform does not have precompiled SDKs available.')
3196 print('You may install components from source.')
3197 print('')
3198
3199 print('All recent (non-legacy) installable versions are:')
3200 releases_versions = sorted(load_releases_versions(), key=version_key, reverse=True)
3201 releases_info = load_releases_info()['releases']
3202 for ver in releases_versions:
3203 print(' %s %s' % (ver, installed_sdk_text('sdk-releases-%s-64bit' % get_release_hash(ver, releases_info))))
3204 print()
3205
3206 # Use array to work around the lack of being able to mutate from enclosing
3207 # function.
3208 has_partially_active_tools = [False]
3209
3210 if sdks:
3211 def find_sdks(needs_compilation):
3212 s = []
3213 for sdk in sdks:
3214 if sdk.is_old and not arg_old:
3215 continue
3216 if sdk.needs_compilation() == needs_compilation:
3217 s += [sdk]
3218 return s
3219
3220 def print_sdks(s):
3221 for sdk in s:
3222 installed = '\tINSTALLED' if sdk.is_installed() else ''
3223 active = '*' if sdk.is_active() else ' '
3224 print(' ' + active + ' {0: <25}'.format(str(sdk)) + installed)
3225 if arg_uses:
3226 for dep in sdk.uses:
3227 print(' - {0: <25}'.format(dep))
3228 print('')
3229 print('The additional following precompiled SDKs are also available for download:')
3230 print_sdks(find_sdks(False))
3231
3232 print('The following SDKs can be compiled from source:')
3233 print_sdks(find_sdks(True))
3234
3235 if tools:
3236 def find_tools(needs_compilation):
3237 t = []
3238 for tool in tools:
3239 if tool.is_old and not arg_old:
3240 continue
3241 if tool.needs_compilation() != needs_compilation:
3242 continue
3243 t += [tool]
3244 return t
3245
3246 def print_tools(t):
3247 for tool in t:
3248 if tool.is_old and not arg_old:
3249 continue
3250 if tool.can_be_installed() is True:
3251 installed = '\tINSTALLED' if tool.is_installed() else ''
3252 else:
3253 installed = '\tNot available: ' + tool.can_be_installed()
3254 tool_is_active = tool.is_active()
3255 tool_is_env_active = tool_is_active and tool.is_env_active()
3256 if tool_is_env_active:
3257 active = ' * '
3258 elif tool_is_active:
3259 active = '(*)'
3260 has_partially_active_tools[0] = has_partially_active_tools[0] or True
3261 else:
3262 active = ' '
3263 print(' ' + active + ' {0: <25}'.format(str(tool)) + installed)
3264 print('')
3265
3266 print('The following precompiled tool packages are available for download:')
3267 print_tools(find_tools(needs_compilation=False))
3268 print('The following tools can be compiled from source:')
3269 print_tools(find_tools(needs_compilation=True))
3270 else:
3271 if is_emsdk_sourced_from_github():
3272 print("There are no tools available. Run 'git pull' to fetch the latest set of tools.")
3273 else:
3274 print("There are no tools available. Run 'emsdk update' to fetch the latest set of tools.")
3275 print('')
3276
3277 print('Items marked with * are activated for the current user.')
3278 if has_partially_active_tools[0]:
3279 env_cmd = 'emsdk_env.bat' if WINDOWS else 'source ./emsdk_env.sh'
3280 print(f'Items marked with (*) are selected for use, but your current shell environment is not configured to use them. Type "{env_cmd}" to set up your current shell to use them' + (', or call "emsdk activate --permanent <name_of_sdk>" to permanently activate them.' if WINDOWS else '.'))
3281 if not arg_old:
3282 print('')
3283 print("To access the historical archived versions, type 'emsdk list --old'")
3284
3285 print('')
3286 if is_emsdk_sourced_from_github():
3287 print('Run "git pull" to pull in the latest list.')
3288 else:
3289 print('Run "./emsdk update" to pull in the latest list.')
3290
3291 return 0
3292 elif cmd == 'construct_env':
3293 # Clean up old temp file up front, in case of failure later before we get
3294 # to write out the new one.
3295 tools_to_activate = currently_active_tools()
3296 tools_to_activate = process_tool_list(tools_to_activate)
3297 env_string = construct_env(tools_to_activate, arg_system, arg_permanent)
3298 if CMD or POWERSHELL:
3299 write_set_env_script(env_string)
3300 else:
3301 sys.stdout.write(env_string)
3302 return 0
3303 elif cmd == 'update':
3304 update_emsdk()
3305 if WINDOWS:
3306 # Clean up litter after old emsdk update which may have left this temp
3307 # file around.
3308 rmfile(sdk_path(EMSDK_SET_ENV))
3309 return 0
3310 elif cmd == 'update-tags':
3311 errlog('`update-tags` is not longer needed. To install the latest tot release just run `install tot`')
3312 return 0
3313 elif cmd in ('activate', 'deactivate'):
3314 if arg_permanent:
3315 print('Registering active Emscripten environment permanently')
3316 print('')
3317
3318 tools_to_activate = currently_active_tools()
3319 for arg in args:
3320 tool = find_tool(arg)
3321 if tool is None:
3322 tool = find_sdk(arg)
3323 if tool is None:
3324 error_on_missing_tool(arg)
3325
3326 if cmd == 'activate':
3327 tools_to_activate += [tool]
3328 elif tool in tools_to_activate:
3329 print('Deactivating tool ' + str(tool) + '.')
3330 tools_to_activate.remove(tool)
3331 else:
3332 print(f'Tool "{arg}" was not active, no need to deactivate.')
3333 if not tools_to_activate:
3334 errlog('No tools/SDKs specified to activate! Usage:\n emsdk activate tool/sdk1 [tool/sdk2] [...]')
3335 return 1
3336 active_tools = set_active_tools(tools_to_activate, permanently_activate=arg_permanent, system=arg_system)
3337 if not active_tools:
3338 errlog('No tools/SDKs found to activate! Usage:\n emsdk activate tool/sdk1 [tool/sdk2] [...]')
3339 return 1
3340 if WINDOWS and not arg_permanent:
3341 errlog('The changes made to environment variables only apply to the currently running shell instance. Use the \'emsdk_env.bat\' to re-enter this environment later, or if you\'d like to register this environment permanently, rerun this command with the option --permanent.')
3342 return 0
3343 elif cmd == 'install':
3344 global BUILD_FOR_TESTING, ENABLE_LLVM_ASSERTIONS, CPU_CORES, GIT_CLONE_SHALLOW
3345
3346 # Process args
3347 for i in range(len(args)):
3348 if args[i].startswith('-j'):
3349 multicore = re.match(r'^-j(\d+)$', args[i])
3350 if multicore:
3351 CPU_CORES = int(multicore.group(1))
3352 args[i] = ''
3353 else:
3354 errlog(f'Invalid command line parameter {args[i]} specified!')
3355 return 1
3356 elif args[i] == '--shallow':
3357 GIT_CLONE_SHALLOW = True
3358 args[i] = ''
3359 elif args[i] == '--build-tests':
3360 BUILD_FOR_TESTING = True
3361 args[i] = ''
3362 elif args[i] == '--enable-assertions':
3363 ENABLE_LLVM_ASSERTIONS = 'ON'
3364 args[i] = ''
3365 elif args[i] == '--disable-assertions':
3366 ENABLE_LLVM_ASSERTIONS = 'OFF'
3367 args[i] = ''
3368 args = [x for x in args if x]
3369 if not args:
3370 errlog("Missing parameter. Type 'emsdk install <tool name>' to install a tool or an SDK. Type 'emsdk list' to obtain a list of available tools. Type 'emsdk install latest' to automatically install the newest version of the SDK.")
3371 return 1
3372
3373 if LINUX and ARCH == 'arm64' and args != ['latest']:
3374 errlog('WARNING: arm64-linux binaries are not available for all releases.')
3375 errlog('See https://github.com/emscripten-core/emsdk/issues/547')
3376
3377 for t in args:
3378 tool = find_tool(t)
3379 if tool is None:
3380 tool = find_sdk(t)
3381 if tool is None:
3382 error_on_missing_tool(t)
3383 tool.install()
3384 return 0
3385 elif cmd == 'uninstall':
3386 if not args:
3387 errlog("Syntax error. Call 'emsdk uninstall <tool name>'. Call 'emsdk list' to obtain a list of available tools.")
3388 return 1
3389 tool = find_tool(args[0])
3390 if tool is None:
3391 errlog(f"Error: Tool by name '{args[0]}' was not found.")
3392 return 1
3393 tool.uninstall()
3394 return 0
3395
3396 errlog(f"Unknown command '{cmd}' given! Type 'emsdk help' to get a list of commands.")
3397 return 1
3398
3399
3400 if __name__ == '__main__':
3401 try:
3402 sys.exit(main(sys.argv[1:]))
3403 except KeyboardInterrupt:
3404 exit_with_error('aborted by user, exiting')
3405 sys.exit(1)