| #!/usr/bin/env python3 | |
| # Copyright 2019 The Emscripten Authors. All rights reserved. | |
| # Emscripten is available under two separate licenses, the MIT license and the | |
| # University of Illinois/NCSA Open Source License. Both these licenses can be | |
| # found in the LICENSE file. | |
| import copy | |
| import json | |
| import multiprocessing | |
| import os | |
| import os.path | |
| import platform | |
| import re | |
| import shutil | |
| import stat | |
| import subprocess | |
| import sys | |
| import sysconfig | |
| import tarfile | |
| import zipfile | |
| if os.name == 'nt': | |
| import ctypes.wintypes | |
| import winreg | |
| from urllib.parse import urljoin | |
| from urllib.request import urlopen | |
| if sys.version_info < (3, 10): # ruff: ignore[outdated-version-block] | |
| print(f'error: emsdk requires python 3.10 or above ({sys.executable} {sys.version})', file=sys.stderr) | |
| sys.exit(1) | |
| emsdk_packages_url = 'https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/' | |
| emscripten_releases_repo = 'https://chromium.googlesource.com/emscripten-releases' | |
| emscripten_releases_download_url_template = "https://storage.googleapis.com/webassembly/emscripten-releases-builds/%s/%s/wasm-binaries%s.%s" | |
| # This was previously `master.zip` but we are transitioning to `main` and | |
| # `HEAD.zip` works for both cases. In future we could switch this to | |
| # `main.zip` perhaps. | |
| emsdk_zip_download_url = 'https://github.com/emscripten-core/emsdk/archive/HEAD.zip' | |
| download_dir = 'downloads/' | |
| extra_release_tag = None | |
| def get_env_boolean(name): | |
| env_var = os.getenv(name) | |
| assert env_var in {None, '1', '0'}, f'invalid environment variable setting ${env_var} for ${name}' | |
| return env_var == '1' | |
| # Enable this to do very verbose printing about the different steps that are | |
| # being run. Useful for debugging. | |
| VERBOSE = get_env_boolean('EMSDK_VERBOSE') | |
| QUIET = get_env_boolean('EMSDK_QUIET') | |
| if get_env_boolean('EMSDK_NOTTY'): | |
| TTY_OUTPUT = False | |
| else: | |
| TTY_OUTPUT = sys.stdout.isatty() | |
| def info(msg): | |
| if not QUIET: | |
| print(msg, file=sys.stderr) | |
| def errlog(msg): | |
| print(msg, file=sys.stderr) | |
| def exit_with_error(msg): | |
| errlog(f'error: {msg}') | |
| sys.exit(1) | |
| WINDOWS = False | |
| MINGW = False | |
| MSYS = False | |
| MACOS = False | |
| LINUX = False | |
| os_override = os.environ.get('EMSDK_OS') | |
| if os_override: | |
| if os_override == 'windows': | |
| WINDOWS = True | |
| elif os_override == 'linux': | |
| LINUX = True | |
| elif os_override == 'macos': | |
| MACOS = True | |
| else: | |
| assert False, 'EMSDK_OS must be one of: windows, linux, macos' | |
| else: | |
| if os.name == 'nt' or ('windows' in os.getenv('SYSTEMROOT', '').lower()) or ('windows' in os.getenv('COMSPEC', '').lower()): | |
| WINDOWS = True | |
| msystem = os.getenv('MSYSTEM') | |
| if msystem: | |
| MSYS = True | |
| # Some functions like os.path.normpath() exhibit different behavior between | |
| # different versions of Python, so we need to distinguish between the MinGW | |
| # and MSYS versions of Python | |
| if sysconfig.get_platform() == 'mingw': | |
| MINGW = True | |
| if msystem not in {'MSYS', 'MINGW64'}: | |
| # https://stackoverflow.com/questions/37460073/msys-vs-mingw-internal-environment-variables | |
| errlog(f'Warning: MSYSTEM environment variable is present, and is set to "{msystem}". ' | |
| 'This shell has not been tested with emsdk and may not work.') | |
| if platform.mac_ver()[0]: | |
| MACOS = True | |
| if not MACOS and (platform.system() == 'Linux'): | |
| LINUX = True | |
| UNIX = (MACOS or LINUX) | |
| # Pick which shell of 4 shells to use | |
| POWERSHELL = get_env_boolean('EMSDK_POWERSHELL') | |
| CSH = get_env_boolean('EMSDK_CSH') | |
| CMD = get_env_boolean('EMSDK_CMD') | |
| BASH = get_env_boolean('EMSDK_BASH') | |
| FISH = get_env_boolean('EMSDK_FISH') | |
| if WINDOWS and BASH: | |
| MSYS = True | |
| if not CSH and not POWERSHELL and not BASH and not CMD and not FISH: | |
| # Fall back to default of `cmd` on windows and `bash` otherwise | |
| if WINDOWS and not MSYS: | |
| CMD = True | |
| else: | |
| BASH = True | |
| if WINDOWS: | |
| ENVPATH_SEPARATOR = ';' | |
| else: | |
| ENVPATH_SEPARATOR = ':' | |
| # platform.machine() may return AMD64 on windows, so standardize the case. | |
| machine = os.getenv('EMSDK_ARCH', platform.machine().lower()) | |
| if machine.startswith(('x64', 'amd64', 'x86_64')): | |
| ARCH = 'x86_64' | |
| elif machine.endswith('86'): | |
| ARCH = 'x86' | |
| elif machine.startswith('aarch64') or machine.lower().startswith('arm64'): | |
| ARCH = 'arm64' | |
| elif machine.startswith('arm'): | |
| ARCH = 'arm' | |
| else: | |
| exit_with_error('unknown machine architecture: ' + machine) | |
| # Don't saturate all cores to not steal the whole system, but be aggressive. | |
| CPU_CORES = int(os.getenv('EMSDK_NUM_CORES', max(multiprocessing.cpu_count() - 1, 1))) | |
| CMAKE_BUILD_TYPE_OVERRIDE = None | |
| # If true, perform a --shallow clone of git. | |
| GIT_CLONE_SHALLOW = False | |
| # If true, LLVM backend is built with tests enabled, and Binaryen is built with | |
| # Visual Studio static analyzer enabled. | |
| BUILD_FOR_TESTING = False | |
| # If 'auto', assertions are decided by the build type | |
| # (Release&MinSizeRel=disabled, Debug&RelWithDebInfo=enabled) | |
| # Other valid values are 'ON' and 'OFF' | |
| ENABLE_LLVM_ASSERTIONS = 'auto' | |
| # If true, keeps the downloaded archive files. | |
| KEEP_DOWNLOADS = get_env_boolean('EMSDK_KEEP_DOWNLOADS') | |
| def os_name_short(): | |
| if WINDOWS: | |
| return 'win' | |
| elif LINUX: | |
| return 'linux' | |
| elif MACOS: | |
| return 'mac' | |
| assert False, 'unknown OS' | |
| def os_name(): | |
| if WINDOWS: | |
| return 'windows' | |
| if LINUX: | |
| return 'linux' | |
| if MACOS: | |
| return 'macos' | |
| assert False, 'unknown OS' | |
| def debug_print(msg): | |
| if VERBOSE: | |
| errlog(msg) | |
| def to_unix_path(p): | |
| return p.replace('\\', '/') | |
| EMSDK_PATH = to_unix_path(os.path.dirname(os.path.realpath(__file__))) | |
| EMSDK_SET_ENV = "" | |
| if POWERSHELL: | |
| EMSDK_SET_ENV = os.path.join(EMSDK_PATH, 'emsdk_set_env.ps1') | |
| else: | |
| EMSDK_SET_ENV = os.path.join(EMSDK_PATH, 'emsdk_set_env.bat') | |
| def parse_github_url_and_refspec(url): | |
| """Parse a github URL a tuple of its components. | |
| Parses https://github.com/emscripten-core/emscripten/tree/d6aced8 to the | |
| tuple (https://github.com/emscripten-core/emscripten, d6aced8, emscripten-core) | |
| or https://github.com/emscripten-core/emscripten/commit/00b76f81f6474113fcf540db69297cfeb180347e | |
| to (https://github.com/emscripten-core/emscripten, 00b76f81f6474113fcf540db69297cfeb180347e, emscripten-core) | |
| """ | |
| if not url: | |
| return ('', '', None) | |
| if url.endswith(('/tree/', '/tree', '/commit/', '/commit')): | |
| raise Exception(f'Malformed git URL and refspec {url}!') | |
| if '/tree/' in url: | |
| if url.endswith('/'): | |
| raise Exception(f'Malformed git URL and refspec {url}!') | |
| url, refspec = url.split('/tree/') | |
| remote_name = url.split('/')[-2] | |
| return (url, refspec, remote_name) | |
| elif '/commit/' in url: | |
| if url.endswith('/'): | |
| raise Exception(f'Malformed git URL and refspec {url}!') | |
| url, refspec = url.split('/commit/') | |
| remote_name = url.split('/')[-2] | |
| return (url, refspec, remote_name) | |
| else: | |
| return (url, 'main', None) # Assume the default branch is main in the absence of a refspec | |
| ARCHIVE_SUFFIXES = ('zip', '.tar', '.gz', '.xz', '.tbz2', '.bz2') | |
| def vswhere(version): | |
| program_files = os.getenv('ProgramFiles(x86)') | |
| if not program_files: | |
| program_files = os.environ['ProgramFiles'] | |
| vswhere_path = os.path.join(program_files, 'Microsoft Visual Studio', 'Installer', 'vswhere.exe') | |
| # Source: https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools?view=vs-2022 | |
| tools_arch = 'ARM64' if ARCH == 'arm64' else 'x86.x64' | |
| try: | |
| # The "-products *" allows detection of Build Tools, the "-prerelease" allows detection of Preview version | |
| # of Visual Studio and Build Tools. | |
| stdout = run_get_output([vswhere_path, '-latest', '-products', '*', | |
| '-prerelease', '-version', f'[{version}.0,{version + 1}.0)', | |
| '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.' + tools_arch, | |
| '-property', 'installationPath', '-format', 'json']) | |
| json_output = json.loads(stdout) | |
| return str(json_output[0]['installationPath']) | |
| except Exception: | |
| return '' | |
| CMAKE_GENERATOR = 'Unix Makefiles' | |
| if WINDOWS: | |
| # Detect which CMake generator to use when building on Windows | |
| if '--mingw' in sys.argv: | |
| CMAKE_GENERATOR = 'MinGW Makefiles' | |
| elif '--vs2022' in sys.argv: | |
| CMAKE_GENERATOR = 'Visual Studio 17' | |
| elif '--vs2019' in sys.argv: | |
| CMAKE_GENERATOR = 'Visual Studio 16' | |
| elif vswhere(17): | |
| CMAKE_GENERATOR = 'Visual Studio 17' | |
| elif vswhere(16): | |
| CMAKE_GENERATOR = 'Visual Studio 16' | |
| elif shutil.which('mingw32-make') is not None and shutil.which('g++') is not None: | |
| CMAKE_GENERATOR = 'MinGW Makefiles' | |
| else: | |
| # No detected generator | |
| CMAKE_GENERATOR = '' | |
| sys.argv = [a for a in sys.argv if a not in {'--mingw', '--vs2019', '--vs2022'}] | |
| def cmake_generator_prefix(): | |
| """Computes a suitable path prefix to use when building with a given generator.""" | |
| if CMAKE_GENERATOR == 'Visual Studio 17': | |
| return '_vs2022' | |
| if CMAKE_GENERATOR == 'Visual Studio 16': | |
| return '_vs2019' | |
| elif CMAKE_GENERATOR == 'MinGW Makefiles': | |
| return '_mingw' | |
| # Unix Makefiles do not specify a path prefix for backwards path compatibility | |
| return '' | |
| def remove_tree(d): | |
| """Removes a directory tree even if it was readonly, and doesn't throw exception | |
| on failure. | |
| """ | |
| debug_print(f'remove_tree({d})') | |
| if not os.path.exists(d): | |
| return | |
| try: | |
| def remove_readonly_and_try_again(func, path, exc_info): | |
| if not (os.stat(path).st_mode & stat.S_IWRITE): | |
| os.chmod(path, stat.S_IWRITE) | |
| func(path) | |
| else: | |
| raise exc_info[1] | |
| shutil.rmtree(d, onerror=remove_readonly_and_try_again) | |
| except Exception as e: | |
| debug_print('remove_tree threw an exception, ignoring: ' + str(e)) | |
| def win_set_environment_variable_direct(key, value, system=True): | |
| folder = None | |
| try: | |
| if system: | |
| # Read globally from ALL USERS section. | |
| folder = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 0, winreg.KEY_ALL_ACCESS) | |
| else: | |
| # Register locally from CURRENT USER section. | |
| folder = winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, 'Environment', 0, winreg.KEY_ALL_ACCESS) | |
| winreg.SetValueEx(folder, key, 0, winreg.REG_EXPAND_SZ, value) | |
| debug_print(f'Set key={key} with value {value} in registry.') | |
| return True | |
| except Exception as e: | |
| # 'Access is denied.' | |
| if e.args[3] == 5: | |
| 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.') | |
| errlog(f'Failed to write environment variable {key}:') | |
| errlog(str(e)) | |
| return False | |
| finally: | |
| if folder is not None: | |
| folder.Close() | |
| def win_get_environment_variable(key, system=True, user=True, fallback=True): | |
| if (not system and not user and fallback): | |
| # if no --system or --permanent flag is provided use shell's value | |
| return os.environ[key] | |
| try: | |
| folder = None | |
| try: | |
| if system: | |
| # Read globally from ALL USERS section. | |
| folder = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment') | |
| else: | |
| # Register locally from CURRENT USER section. | |
| folder = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Environment') | |
| value = str(winreg.QueryValueEx(folder, key)[0]) | |
| except Exception: | |
| # If reading registry fails for some reason - read via os.environ. This has the drawback | |
| # that expansion items such as %PROGRAMFILES% will have been expanded, so | |
| # need to be precise not to set these back to system registry, or | |
| # expansion items would be lost. | |
| if fallback: | |
| return os.environ[key] | |
| return None | |
| finally: | |
| if folder is not None: | |
| folder.Close() | |
| except Exception as e: | |
| # this catch is if both the registry key threw an exception and the key is not in os.environ | |
| if e.args[0] != 2: | |
| # 'The system cannot find the file specified.' | |
| errlog(f'Failed to read environment variable {key}:') | |
| errlog(str(e)) | |
| return None | |
| return value | |
| def win_set_environment_variable(key, value, system, user): | |
| debug_print(f'set {key}={value}, in system={system}') | |
| previous_value = win_get_environment_variable(key, system=system, user=user) | |
| if previous_value == value: | |
| debug_print(' no need to set, since same value already exists.') | |
| # No need to elevate UAC for nothing to set the same value, skip. | |
| return False | |
| if not value: | |
| try: | |
| if system: | |
| cmd = ['REG', 'DELETE', 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', '/V', key, '/f'] | |
| else: | |
| cmd = ['REG', 'DELETE', 'HKCU\\Environment', '/V', key, '/f'] | |
| debug_print(str(cmd)) | |
| subprocess.call(cmd, stdout=subprocess.PIPE) | |
| except Exception: | |
| return False | |
| return True | |
| try: | |
| if win_set_environment_variable_direct(key, value, system): | |
| return True | |
| # Escape % signs so that we don't expand references to environment variables. | |
| value = value.replace('%', '^%') | |
| if len(value) >= 1024: | |
| 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.') | |
| cmd = ['SETX', key, value] | |
| debug_print(str(cmd)) | |
| retcode = subprocess.call(cmd, stdout=subprocess.PIPE) | |
| if retcode != 0: | |
| errlog(f'ERROR! Failed to set environment variable {key}={value}. You may need to set it manually.') | |
| else: | |
| return True | |
| except Exception as e: | |
| errlog(f'ERROR! Failed to set environment variable {key}={value}:') | |
| errlog(str(e)) | |
| errlog('You may need to set it manually.') | |
| return False | |
| def win_set_environment_variables(env_vars_to_add, system, user): | |
| if not env_vars_to_add: | |
| return | |
| changed = False | |
| for key, value in env_vars_to_add: | |
| if win_set_environment_variable(key, value, system, user): | |
| if not changed: | |
| changed = True | |
| print('Setting global environment variables:') | |
| print(f'{key} = {value}') | |
| if not changed: | |
| print('Global environment variables up to date') | |
| return | |
| # if changes were made then we need to notify other processes | |
| try: | |
| HWND_BROADCAST = ctypes.wintypes.HWND(0xFFFF) # win32con.HWND_BROADCAST == 65535 | |
| WM_SETTINGCHANGE = 0x001A # win32con.WM_SETTINGCHANGE == 26 | |
| SMTO_BLOCK = 0x0001 # win32con.SMTO_BLOCK == 1 | |
| ctypes.windll.user32.SendMessageTimeoutA( | |
| HWND_BROADCAST, # hWnd: notify everyone | |
| WM_SETTINGCHANGE, # Msg: registry changed | |
| 0, # wParam: Must be 0 when setting changed is sent by users | |
| 'Environment', # lParam: Specifically environment variables changed | |
| SMTO_BLOCK, # fuFlags: Wait for message to be sent or timeout | |
| 100) # uTimeout: 100ms | |
| except Exception as e: | |
| errlog('SendMessageTimeout failed with error: ' + str(e)) | |
| def win_delete_environment_variable(key, system=True, user=True): | |
| debug_print(f'win_delete_environment_variable(key={key}, system={system})') | |
| return win_set_environment_variable(key, None, system, user) | |
| def sdk_path(path): | |
| """Returns the absolute pathname to the given path inside the Emscripten SDK.""" | |
| if os.path.isabs(path): | |
| return path | |
| return to_unix_path(os.path.join(EMSDK_PATH, path)) | |
| def rmfile(filename): | |
| """Removes a single file, suppressing exceptions on failure.""" | |
| debug_print(f'rmfile({filename})') | |
| if os.path.lexists(filename): | |
| os.remove(filename) | |
| def mkdir_p(path): | |
| debug_print(f'mkdir_p({path})') | |
| os.makedirs(path, exist_ok=True) | |
| def is_nonempty_directory(path): | |
| if not os.path.isdir(path): | |
| return False | |
| return len(os.listdir(path)) != 0 | |
| def run(cmd, cwd=None, check=True): | |
| debug_print(f'run(cmd={cmd}, cwd={cwd})') | |
| returncode = subprocess.call(cmd, cwd=cwd, env=os.environ.copy()) | |
| if returncode != 0 and check: | |
| errlog(f'{cmd} failed with error code {returncode}') | |
| return returncode | |
| def untargz(source_filename, dest_dir): | |
| print(f"Unpacking '{source_filename}' to '{dest_dir}'") | |
| mkdir_p(dest_dir) | |
| returncode = run(['tar', '-xvf' if VERBOSE else '-xf', sdk_path(source_filename), '--strip', '1'], cwd=dest_dir) | |
| # tfile = tarfile.open(source_filename, 'r:gz') | |
| # tfile.extractall(dest_dir) | |
| return returncode == 0 | |
| def fix_potentially_long_windows_pathname(pathname): | |
| """Convert pathname to use extended-length path prefix on windows. | |
| On Windows, it is not possible to reference path names that are longer than | |
| ~260 characters, unless the path is referenced via a "\\?\" prefix. | |
| See https://msdn.microsoft.com/en-us/library/aa365247.aspx#maxpath and | |
| http://stackoverflow.com/questions/3555527/python-win32-filename-length-workaround | |
| In that mode, forward slashes cannot be used as delimiters. | |
| """ | |
| if (not WINDOWS or MSYS) or os_override: | |
| return pathname | |
| # Test if emsdk calls fix_potentially_long_windows_pathname() with long | |
| # relative paths (which is problematic) | |
| if not os.path.isabs(pathname) and len(pathname) > 200: | |
| 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!') | |
| if pathname.startswith('\\\\?\\'): | |
| return pathname | |
| pathname = os.path.normpath(pathname.replace('/', '\\')) | |
| if MINGW: | |
| # MinGW versions of Python return normalized paths with backslashes | |
| # converted to forward slashes, so we must use forward slashes in our | |
| # prefix | |
| return '//?/' + pathname | |
| return '\\\\?\\' + pathname | |
| def move_with_overwrite(src, dest): | |
| """On windows, rename/move will fail if the destination exists, and there is no | |
| race-free way to do it. This method removes the destination if it exists, so | |
| the move always works | |
| """ | |
| if os.path.exists(dest): | |
| os.remove(dest) | |
| os.rename(src, dest) | |
| # http://stackoverflow.com/questions/12886768/simple-way-to-unzip-file-in-python-on-all-oses | |
| def unzip(source_filename, dest_dir): | |
| print(f"Unpacking '{source_filename}' to '{dest_dir}'") | |
| mkdir_p(dest_dir) | |
| common_subdir = None | |
| try: | |
| with zipfile.ZipFile(source_filename) as zf: | |
| # Implement '--strip 1' behavior to unzipping by testing if all the files | |
| # in the zip reside in a common subdirectory, and if so, we move the | |
| # output tree at the end of uncompression step. | |
| for member in zf.infolist(): | |
| words = member.filename.split('/') | |
| if len(words) > 1: # If there is a directory component? | |
| if common_subdir is None: | |
| common_subdir = words[0] | |
| elif common_subdir != words[0]: | |
| common_subdir = None | |
| break | |
| else: | |
| common_subdir = None | |
| break | |
| unzip_to_dir = dest_dir | |
| if common_subdir: | |
| unzip_to_dir = os.path.join(os.path.dirname(dest_dir), 'unzip_temp') | |
| # Now do the actual decompress. | |
| unzip_to_dir = fix_potentially_long_windows_pathname(unzip_to_dir) | |
| for member in zf.infolist(): | |
| zf.extract(member, unzip_to_dir) | |
| dst_filename = os.path.join(unzip_to_dir, os.path.normpath(member.filename)) | |
| # See: https://stackoverflow.com/questions/42326428/zipfile-in-python-file-permission | |
| unix_attributes = member.external_attr >> 16 | |
| if unix_attributes: | |
| os.chmod(dst_filename, unix_attributes) | |
| # Move the extracted file to its final location without the base | |
| # directory name, if we are stripping that away. | |
| if common_subdir: | |
| assert member.filename.startswith(common_subdir), f'unexpected filename {member.filename}' | |
| stripped_filename = '.' + member.filename[len(common_subdir):] | |
| final_dst_filename = os.path.join(dest_dir, stripped_filename) | |
| # Check if a directory | |
| if stripped_filename.endswith('/'): | |
| d = fix_potentially_long_windows_pathname(final_dst_filename) | |
| if not os.path.isdir(d): | |
| os.mkdir(d) | |
| else: | |
| parent_dir = os.path.dirname(fix_potentially_long_windows_pathname(final_dst_filename)) | |
| if parent_dir and not os.path.exists(parent_dir): | |
| os.makedirs(parent_dir) | |
| move_with_overwrite(fix_potentially_long_windows_pathname(dst_filename), fix_potentially_long_windows_pathname(final_dst_filename)) | |
| if common_subdir: | |
| remove_tree(unzip_to_dir) | |
| except zipfile.BadZipfile as e: | |
| errlog(f"Unzipping file '{source_filename}' failed due to reason: {e}! Removing the corrupted zip file.") | |
| rmfile(source_filename) | |
| return False | |
| except Exception as e: | |
| errlog(f"Unzipping file '{source_filename}' failed due to reason: {e}") | |
| return False | |
| return True | |
| def path_points_to_directory(path): | |
| """This function interprets whether the given string looks like a path to a | |
| directory instead of a file, without looking at the actual filesystem. | |
| 'a/b/c' points to directory, so does 'a/b/c/', but 'a/b/c.x' is parsed as a | |
| filename | |
| """ | |
| if path == '.': | |
| return True | |
| last_slash = max(path.rfind('/'), path.rfind('\\')) | |
| last_dot = path.rfind('.') | |
| no_suffix = last_dot < last_slash or last_dot == -1 | |
| if no_suffix: | |
| return True | |
| suffix = path[last_dot:] | |
| # Very simple logic for the only file suffixes used by emsdk downloader. Other | |
| # suffixes, like 'clang-3.2' are treated as dirs. | |
| if suffix in {'.exe', '.zip', '.txt'}: | |
| return False | |
| else: | |
| return True | |
| def get_content_length(download): | |
| try: | |
| return int(download.headers.get('Content-Length', 0)) | |
| except ValueError: | |
| return 0 | |
| def get_download_target(url, dstpath, filename_prefix=''): | |
| file_name = filename_prefix + url.split('/')[-1] | |
| if path_points_to_directory(dstpath): | |
| file_name = os.path.join(dstpath, file_name) | |
| else: | |
| file_name = dstpath | |
| # Treat all relative destination paths as relative to the SDK root directory, | |
| # not the current working directory. | |
| file_name = sdk_path(file_name) | |
| return file_name | |
| def download_with_curl(url, file_name): | |
| print(f"Downloading: {file_name} from {url}") | |
| if not shutil.which('curl'): | |
| exit_with_error('curl not found in PATH') | |
| # -#: show progress bar | |
| # -L: Follow HTTP 3XX redirections | |
| # -f: Fail on HTTP errors | |
| subprocess.check_call(['curl', '-#', '-f', '-L', '-o', file_name, url]) | |
| def download_with_urllib(url, file_name): | |
| u = urlopen(url) | |
| file_size = get_content_length(u) | |
| if file_size > 0: | |
| print(f"Downloading: {file_name} from {url}, {file_size} Bytes") | |
| else: | |
| print(f"Downloading: {file_name} from {url}") | |
| file_size_dl = 0 | |
| # Draw a progress bar 80 chars wide (in non-TTY mode) | |
| progress_max = 80 - 4 | |
| progress_shown = 0 | |
| block_sz = 256 * 1024 | |
| if not TTY_OUTPUT: | |
| print(' [', end='') | |
| with open(file_name, 'wb') as f: | |
| while True: | |
| buffer = u.read(block_sz) | |
| if not buffer: | |
| break | |
| file_size_dl += len(buffer) | |
| f.write(buffer) | |
| if file_size: | |
| percent = file_size_dl * 100.0 / file_size | |
| if TTY_OUTPUT: | |
| status = f' {file_size_dl:10d} [{percent:3.02f}%]' | |
| print(status, end='\r') | |
| else: | |
| while progress_shown < progress_max * percent / 100: | |
| print('-', end='') | |
| sys.stdout.flush() | |
| progress_shown += 1 | |
| if not TTY_OUTPUT: | |
| print(']') | |
| sys.stdout.flush() | |
| debug_print(f'finished downloading ({file_size_dl} bytes)') | |
| def download_file(url, dstpath, filename_prefix=''): | |
| """On success, returns the filename on the disk pointing to the destination file that was produced | |
| On failure, returns None. | |
| """ | |
| debug_print(f'download_file(url={url}, dstpath={dstpath})') | |
| file_name = get_download_target(url, dstpath, filename_prefix) | |
| if KEEP_DOWNLOADS and os.path.exists(file_name): | |
| print(f"File '{file_name}' already downloaded, skipping.") | |
| return file_name | |
| mkdir_p(os.path.dirname(file_name)) | |
| try: | |
| # Use curl on macOS or when EMSDK_USE_CURL is set to avoid | |
| # CERTIFICATE_VERIFY_FAILED issue with python's urllib: | |
| # https://stackoverflow.com/questions/40684543/how-to-make-python-use-ca-certificates-from-mac-os-truststore | |
| # Unlike on linux or windows, curl is always available on macOS systems. | |
| if MACOS or 'EMSDK_USE_CURL' in os.environ: | |
| download_with_curl(url, file_name) | |
| else: | |
| download_with_urllib(url, file_name) | |
| except Exception as e: | |
| errlog(f"Error: Downloading URL '{url}': {e}") | |
| return None | |
| except KeyboardInterrupt: | |
| rmfile(file_name) | |
| raise | |
| return file_name | |
| def run_get_output(cmd, cwd=None): | |
| debug_print(f'run_get_output(cmd={cmd}, cwd={cwd})') | |
| return subprocess.check_output(cmd, cwd=cwd, text=True) | |
| def GIT(): | |
| git = shutil.which('git') | |
| if not git: | |
| msg = 'git executable was not found. Please install git for this operation!' | |
| if WINDOWS: | |
| msg += "This can be done from http://git-scm.com/" | |
| elif MACOS: | |
| msg += "This can be done from http://git-scm.com/, or by installing XCode and then the XCode Command Line Tools" | |
| elif LINUX: | |
| msg += "This can be probably be done using your package manager, see http://git-scm.com/book/en/Getting-Started-Installing-Git" | |
| exit_with_error(msg) | |
| return git | |
| def git_repo_version(repo_path): | |
| stdout = run_get_output([GIT(), 'log', '-n', '1', '--pretty="%aD %H"'], cwd=repo_path) | |
| return stdout.strip() | |
| def git_recent_commits(repo_path, n=20): | |
| stdout = run_get_output([GIT(), 'log', '-n', str(n), '--pretty="%H"'], cwd=repo_path) | |
| return stdout.strip().replace('\r', '').replace('"', '').split('\n') | |
| def get_git_remotes(repo_path): | |
| remotes = [] | |
| output = run_get_output([GIT(), 'remote', '-v'], cwd=repo_path) | |
| for line in output.splitlines(): | |
| remotes += [line.split()[0]] | |
| return remotes | |
| def git_clone(url, dstpath, branch, remote_name='origin'): | |
| debug_print(f'git_clone(url={url}, dstpath={dstpath})') | |
| if os.path.exists(os.path.join(dstpath, '.git')): | |
| remotes = get_git_remotes(dstpath) | |
| if remote_name in remotes: | |
| debug_print(f'Repository {url} with remote "{remote_name}" already cloned to directory {dstpath}, skipping.') | |
| return True | |
| else: | |
| debug_print(f'Repository {url} with remote "{remote_name}" already cloned to directory {dstpath}, but remote has not yet been added. Creating.') | |
| return run([GIT(), 'remote', 'add', remote_name, url], cwd=dstpath) == 0 | |
| mkdir_p(dstpath) | |
| git_clone_args = ['--recurse-submodules', '--branch', branch] # Do not check out a branch (installer will issue a checkout command right after) | |
| if GIT_CLONE_SHALLOW: | |
| git_clone_args += ['--depth', '1'] | |
| print(f'Cloning from {url}...') | |
| return run([GIT(), 'clone', '-o', remote_name, *git_clone_args, url, dstpath]) == 0 | |
| def git_pull(repo_path, branch_or_tag, remote_name='origin'): | |
| debug_print(f'git_pull(repo_path={repo_path}, branch/tag={branch_or_tag}, remote_name={remote_name})') | |
| ret = run([GIT(), 'fetch', '--quiet', remote_name], repo_path) | |
| if ret != 0: | |
| return False | |
| try: | |
| print(f"Fetching latest changes to the branch/tag '{branch_or_tag}' for '{repo_path}'...") | |
| ret = run([GIT(), 'fetch', '--quiet', remote_name], repo_path) | |
| if ret != 0: | |
| return False | |
| # Test if branch_or_tag is a branch, or if it is a tag that needs to be updated | |
| target_is_tag = run([GIT(), 'symbolic-ref', '-q', 'HEAD'], repo_path, check=False) | |
| if target_is_tag: | |
| ret = run([GIT(), 'checkout', '--recurse-submodules', '--quiet', branch_or_tag], repo_path) | |
| else: | |
| local_branch_prefix = (remote_name + '_') if remote_name != 'origin' else '' | |
| ret = run([GIT(), 'checkout', '--recurse-submodules', '--quiet', '-B', local_branch_prefix + branch_or_tag, | |
| '--track', remote_name + '/' + branch_or_tag], repo_path) | |
| if ret != 0: | |
| return False | |
| if not target_is_tag: | |
| # update branch to latest (not needed for tags) | |
| # this line assumes that the user has not gone and made local changes to the repo | |
| ret = run([GIT(), 'merge', '--ff-only', remote_name + '/' + branch_or_tag], repo_path) | |
| if ret != 0: | |
| return False | |
| run([GIT(), 'submodule', 'update', '--init'], repo_path, check=False) | |
| except Exception: | |
| errlog('git operation failed!') | |
| return False | |
| print(f"Successfully updated and checked out branch/tag '{branch_or_tag}' on repository '{repo_path}'") | |
| print("Current repository version: " + git_repo_version(repo_path)) | |
| return True | |
| def git_clone_checkout_and_pull(url, dstpath, branch, override_remote_name='origin'): | |
| debug_print(f'git_clone_checkout_and_pull(url={url}, dstpath={dstpath}, branch={branch}, override_remote_name={override_remote_name})') | |
| # Make sure the repository is cloned first | |
| success = git_clone(url, dstpath, branch, override_remote_name) | |
| if not success: | |
| return False | |
| # And/or issue a pull/checkout to get to latest code. | |
| return git_pull(dstpath, branch, override_remote_name) | |
| def decide_cmake_build_type(tool): | |
| """Each tool can have its own build type, or it can be overridden on the command | |
| line. | |
| """ | |
| if CMAKE_BUILD_TYPE_OVERRIDE: | |
| return CMAKE_BUILD_TYPE_OVERRIDE | |
| else: | |
| return tool.cmake_build_type | |
| def llvm_build_dir(tool): | |
| """The root directory of the build.""" | |
| generator_suffix = cmake_generator_prefix() | |
| bitness_suffix = '_32' if tool.bitness == 32 else '_64' | |
| if tool.git_branch: | |
| build_dir = 'build_' + tool.git_branch.replace(os.sep, '-') + generator_suffix + bitness_suffix | |
| else: | |
| build_dir = 'build_' + tool.version + generator_suffix + bitness_suffix | |
| return build_dir | |
| def exe_suffix(filename): | |
| if WINDOWS and not filename.endswith('.exe'): | |
| filename += '.exe' | |
| return filename | |
| def llvm_build_bin_dir(tool): | |
| """The directory where the binaries are produced. (relative to the installation | |
| root directory of the tool) | |
| """ | |
| build_dir = llvm_build_dir(tool) | |
| if WINDOWS and 'Visual Studio' in CMAKE_GENERATOR: | |
| old_llvm_bin_dir = os.path.join(build_dir, 'bin', decide_cmake_build_type(tool)) | |
| new_llvm_bin_dir = None | |
| default_cmake_build_type = decide_cmake_build_type(tool) | |
| cmake_build_types = [default_cmake_build_type, 'Release', 'RelWithDebInfo', 'MinSizeRel', 'Debug'] | |
| for build_type in cmake_build_types: | |
| d = os.path.join(build_dir, build_type, 'bin') | |
| if os.path.isfile(os.path.join(tool.installation_path(), d, exe_suffix('clang'))): | |
| new_llvm_bin_dir = d | |
| break | |
| if new_llvm_bin_dir and os.path.exists(os.path.join(tool.installation_path(), new_llvm_bin_dir)): | |
| return new_llvm_bin_dir | |
| elif os.path.exists(os.path.join(tool.installation_path(), old_llvm_bin_dir)): | |
| return old_llvm_bin_dir | |
| return os.path.join(build_dir, default_cmake_build_type, 'bin') | |
| else: | |
| return os.path.join(build_dir, 'bin') | |
| def build_env(): | |
| if WINDOWS and 'Visual Studio' in CMAKE_GENERATOR: | |
| env = os.environ.copy() | |
| # MSBuild.exe has an internal mechanism to avoid N^2 oversubscription of threads in its two-tier build model, see | |
| # https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/ | |
| env['UseMultiToolTask'] = 'true' | |
| env['EnforceProcessCountAcrossBuilds'] = 'true' | |
| return env | |
| # By default, just inherit the parent env | |
| return None | |
| def find_cmake(): | |
| """Find path to cmake executable, as one of the activated tools, in PATH, or from installed tools.""" | |
| def locate_cmake_from_tool(tool): | |
| tool_path = get_required_path([tool]) | |
| tool_path = tool_path[-1] | |
| cmake_exe = 'cmake.exe' if WINDOWS else 'cmake' | |
| cmake_exe = os.path.join(tool_path, cmake_exe) | |
| if os.path.isfile(cmake_exe): | |
| return cmake_exe | |
| # 1. If user has activated a specific cmake tool, then use that tool to configure the build. | |
| for tool in reversed(tools): | |
| if tool.id == 'cmake' and tool.is_active(): | |
| cmake_exe = locate_cmake_from_tool(tool) | |
| if cmake_exe: | |
| info(f'Found installed+activated CMake tool at "{cmake_exe}"') | |
| return cmake_exe | |
| # 2. If cmake already exists in PATH, then use that cmake to configure the build. | |
| cmake_exe = shutil.which('cmake') | |
| if cmake_exe: | |
| info(f'Found CMake from PATH at "{cmake_exe}"') | |
| return cmake_exe | |
| # 3. Finally, if user has installed a cmake tool, but has not activated that, then use | |
| # that tool. This enables a single-liner directive | |
| # "emsdk install cmake-4.2.0-rc3-64bit llvm-git-main-64bit" to first install CMake, and | |
| # then use it to configure to build LLVM. | |
| for tool in reversed(tools): | |
| if tool.id == 'cmake' and tool.is_installed(): | |
| cmake_exe = locate_cmake_from_tool(tool) | |
| if cmake_exe: | |
| info(f'Found installed CMake tool at "{cmake_exe}"') | |
| return cmake_exe | |
| exit_with_error('Unable to find "cmake" in PATH, or as installed/activated tool! Please install CMake first') | |
| def make_build(build_root, build_type): | |
| debug_print(f'make_build(build_root={build_root}, build_type={build_type})') | |
| if CPU_CORES > 1: | |
| print(f'Performing a parallel build with {CPU_CORES} cores.') | |
| else: | |
| print('Performing a singlethreaded build.') | |
| make = [find_cmake(), '--build', '.', '--config', build_type] | |
| if 'Visual Studio' in CMAKE_GENERATOR: | |
| # Visual Studio historically has had a two-tier problem in its build system design. A single MSBuild.exe instance only governs | |
| # the build of a single project (.exe/.lib/.dll) in a solution. Passing the -j parameter above will only enable multiple MSBuild.exe | |
| # instances to be spawned to build multiple projects in parallel, but each MSBuild.exe is still singlethreaded. | |
| # To enable each MSBuild.exe instance to also compile several .cpp files in parallel inside a single project, pass the extra | |
| # MSBuild.exe specific "Multi-ToolTask" (MTT) setting /p:CL_MPCount. This enables each MSBuild.exe to parallelize builds wide. | |
| # This requires CMake 3.12 or newer. | |
| make += ['-j', str(CPU_CORES), '--', '/p:CL_MPCount=' + str(CPU_CORES)] | |
| else: | |
| # Pass -j to native make, CMake might not support -j option. | |
| make += ['--', '-j', str(CPU_CORES)] | |
| # Build | |
| print('Running build: ' + str(make)) | |
| ret = subprocess.call(make, cwd=build_root, env=build_env()) | |
| if ret != 0: | |
| errlog(f'Build failed with exit code {ret}!') | |
| errlog('Working directory: ' + build_root) | |
| return False | |
| return True | |
| def write_file(filename, content): | |
| with open(filename, 'w', encoding='utf-8') as f: | |
| f.write(content) | |
| def read_file(filename): | |
| with open(filename, encoding='utf-8') as f: | |
| return f.read() | |
| def cmake_configure(generator, build_root, src_root, build_type, extra_cmake_args): | |
| debug_print(f'cmake_configure(generator={generator}, build_root={build_root}, ' | |
| f'src_root={src_root}, build_type={build_type}, extra_cmake_args={extra_cmake_args})') | |
| # Configure | |
| if not os.path.isdir(build_root): | |
| # Create build output directory if it doesn't yet exist. | |
| os.mkdir(build_root) | |
| cmdline = [find_cmake(), '-DCMAKE_BUILD_TYPE=' + build_type, '-DPYTHON_EXECUTABLE=' + sys.executable] | |
| if generator: | |
| cmdline += ['-G', generator] | |
| # Target macOS 11.0 Big Sur at minimum, to support older Mac devices. | |
| # See https://en.wikipedia.org/wiki/MacOS#Hardware_compatibility for min-spec details. | |
| cmdline += ['-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0'] | |
| cmdline += [*extra_cmake_args, src_root] | |
| # If we have a bootstrap compiler package installed, then use that compiler | |
| # in the cmake build. | |
| for ver in ['16.0.6', '15.0.7']: | |
| bootstrap_compiler_path = os.path.abspath(f'llvm/{ver}/build_llvmorg-{ver}_64/bin') | |
| bs_clang = os.path.join(bootstrap_compiler_path, 'clang') | |
| bs_clangxx = os.path.join(bootstrap_compiler_path, 'clang++') | |
| if os.path.isfile(bs_clang) and f'llvm/{ver}' not in build_root: | |
| info(f'Using bootstrap compiler from {bs_clang} in CMake configure') | |
| cmdline += [f'-DCMAKE_C_COMPILER={bs_clang}', f'-DCMAKE_CXX_COMPILER={bs_clangxx}'] | |
| break | |
| print(f'Running CMake: {cmdline}') | |
| # Specify the deployment target also as an env. var, since some Xcode versions | |
| # read this instead of the CMake field. | |
| os.environ['MACOSX_DEPLOYMENT_TARGET'] = '11.0' | |
| def quote_parens(x): | |
| if ' ' in x: | |
| return '"' + x.replace('"', '\\"') + '"' | |
| else: | |
| return x | |
| # Create a file 'recmake.bat/sh' in the build root that user can call to | |
| # manually recmake the build tree with the previous build params | |
| re_cmake_script = os.path.join(build_root, 'recmake.' + ('bat' if WINDOWS else 'sh')) | |
| write_file(re_cmake_script, ' '.join(map(quote_parens, cmdline))) | |
| try: | |
| subprocess.check_call(cmdline, cwd=build_root, env=build_env()) | |
| except Exception as e: | |
| errlog('CMake invocation failed due to exception') | |
| errlog(f'Working directory: {build_root}') | |
| errlog(str(e)) | |
| return False | |
| return True | |
| def xcode_sdk_version(): | |
| try: | |
| output = run_get_output(['xcrun', '--show-sdk-version']) | |
| return output.strip().split('.') | |
| except Exception: | |
| return subprocess.checkplatform.mac_ver()[0].split('.') | |
| def cmake_target_platform(tool): | |
| # Source: https://cmake.org/cmake/help/latest/generator/Visual%20Studio%2017%202022.html#platform-selection | |
| if tool.arch: | |
| if tool.arch == 'arm64': | |
| return 'ARM64' | |
| elif tool.arch == 'x86_64': | |
| return 'x64' | |
| elif tool.arch == 'x86': | |
| return 'Win32' | |
| if ARCH == 'arm64': | |
| return 'ARM64' | |
| else: | |
| return 'x64' if tool.bitness == 64 else 'Win32' | |
| def cmake_host_platform(): | |
| # Source: https://cmake.org/cmake/help/latest/generator/Visual%20Studio%2017%202022.html#toolset-selection | |
| arch_to_cmake_host_platform = { | |
| 'arm64': 'ARM64', | |
| 'arm': 'ARM', | |
| 'x86_64': 'x64', | |
| 'x86': 'x86', | |
| } | |
| return arch_to_cmake_host_platform[ARCH] | |
| def get_generator_and_config_args(tool): | |
| args = [] | |
| cmake_generator = CMAKE_GENERATOR | |
| if 'Visual Studio 16' in CMAKE_GENERATOR or 'Visual Studio 17' in CMAKE_GENERATOR: # VS2019 or VS2022 | |
| # With Visual Studio 16 2019, CMake changed the way they specify target arch. | |
| # Instead of appending it into the CMake generator line, it is specified | |
| # with a -A arch parameter. | |
| args += ['-A', cmake_target_platform(tool)] | |
| args += ['-Thost=' + cmake_host_platform()] | |
| elif 'Visual Studio' in CMAKE_GENERATOR and tool.bitness == 64: | |
| cmake_generator += ' Win64' | |
| args += ['-Thost=x64'] | |
| return (cmake_generator, args) | |
| def cmake_configure_and_build(cmake_generator, build_root, cmakelists_dir, build_type, args): | |
| success = cmake_configure(cmake_generator, build_root, cmakelists_dir, build_type, args) | |
| if success: | |
| success = make_build(build_root, build_type) | |
| if not success and get_env_boolean('EMSDK_RETRY_CLEAN_BUILD'): | |
| # Delete the CMake build directory to force a full re-cmake + clean rebuild. | |
| shutil.rmtree(build_root) | |
| # And re-try configure + build again. | |
| success = cmake_configure(cmake_generator, build_root, cmakelists_dir, build_type, args) | |
| if success: | |
| success = make_build(build_root, build_type) | |
| return success | |
| def build_llvm(tool): | |
| debug_print(f'build_llvm({tool})') | |
| llvm_root = tool.installation_path() | |
| llvm_src_root = os.path.join(llvm_root, 'src') | |
| success = git_clone_checkout_and_pull(tool.url, llvm_src_root, tool.git_branch) | |
| if not success: | |
| return False | |
| build_dir = llvm_build_dir(tool) | |
| build_root = os.path.join(llvm_root, build_dir) | |
| build_type = decide_cmake_build_type(tool) | |
| # Configure | |
| tests_arg = 'ON' if BUILD_FOR_TESTING else 'OFF' | |
| enable_assertions = ENABLE_LLVM_ASSERTIONS.lower() == 'on' or (ENABLE_LLVM_ASSERTIONS == 'auto' and build_type.lower() not in {'release', 'minsizerel'}) | |
| if ARCH in {'x86', 'x86_64'}: | |
| targets_to_build = 'WebAssembly;X86' | |
| elif ARCH == 'arm': | |
| targets_to_build = 'WebAssembly;ARM' | |
| elif ARCH == 'arm64': | |
| targets_to_build = 'WebAssembly;AArch64' | |
| else: | |
| targets_to_build = 'WebAssembly' | |
| cmake_generator, args = get_generator_and_config_args(tool) | |
| args += ['-DLLVM_TARGETS_TO_BUILD=' + targets_to_build, | |
| '-DLLVM_INCLUDE_EXAMPLES=OFF', | |
| '-DLLVM_INCLUDE_TESTS=' + tests_arg, | |
| '-DCLANG_INCLUDE_TESTS=' + tests_arg, | |
| '-DLLVM_ENABLE_ASSERTIONS=' + ('ON' if enable_assertions else 'OFF'), | |
| # Disable optional LLVM dependencies, these can cause unwanted .so dependencies | |
| # that prevent distributing the generated compiler for end users. | |
| '-DLLVM_ENABLE_LIBXML2=OFF', '-DLLDB_ENABLE_LIBEDIT=OFF', | |
| '-DLLVM_ENABLE_LIBEDIT=OFF', '-DLLVM_ENABLE_LIBPFM=OFF'] | |
| # 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 | |
| # to reroute that to build_root directory then fails on an error | |
| # file INSTALL cannot find | |
| # "C:/code/emsdk/llvm/git/build_master_vs2017_64/$(Configuration)/lib/clang/10.0.0/lib/windows/clang_rt.ubsan_standalone-x86_64.lib". | |
| # (there instead of $(Configuration), one would need ${CMAKE_BUILD_TYPE} ?) | |
| # It looks like compiler-rt is not compatible to build on Windows? | |
| args += ['-DLLVM_ENABLE_PROJECTS=clang;lld'] | |
| # To enable widest possible chance of success for building, let the code | |
| # compile through with older toolchains that are about to be deprecated by | |
| # upstream LLVM. | |
| args += ['-DLLVM_TEMPORARILY_ALLOW_OLD_TOOLCHAIN=ON'] | |
| if os.getenv('LLVM_CMAKE_ARGS'): | |
| extra_args = os.environ['LLVM_CMAKE_ARGS'].split(',') | |
| print('Passing the following extra arguments to LLVM CMake configuration: ' + str(extra_args)) | |
| args += extra_args | |
| cmakelists_dir = os.path.join(llvm_src_root, 'llvm') | |
| success = cmake_configure_and_build(cmake_generator, build_root, cmakelists_dir, build_type, args) | |
| return success | |
| def build_ninja(tool): | |
| debug_print(f'build_ninja({tool})') | |
| root = os.path.normpath(tool.installation_path()) | |
| src_root = os.path.join(root, 'src') | |
| success = git_clone_checkout_and_pull(tool.url, src_root, tool.git_branch) | |
| if not success: | |
| return False | |
| build_dir = llvm_build_dir(tool) | |
| build_root = os.path.join(root, build_dir) | |
| build_type = decide_cmake_build_type(tool) | |
| # Configure | |
| cmake_generator, args = get_generator_and_config_args(tool) | |
| cmakelists_dir = os.path.join(src_root) | |
| success = cmake_configure_and_build(cmake_generator, build_root, cmakelists_dir, build_type, args) | |
| if success: | |
| bin_dir = os.path.join(root, 'bin') | |
| mkdir_p(bin_dir) | |
| exe_paths = [os.path.join(build_root, 'Release', 'ninja'), os.path.join(build_root, 'ninja')] | |
| for e in exe_paths: | |
| for s in ['.exe', '']: | |
| ninja = e + s | |
| if os.path.isfile(ninja): | |
| dst = os.path.join(bin_dir, 'ninja' + s) | |
| shutil.copyfile(ninja, dst) | |
| os.chmod(dst, os.stat(dst).st_mode | stat.S_IEXEC) | |
| return success | |
| def build_ccache(tool): | |
| debug_print(f'build_ccache({tool})') | |
| root = os.path.normpath(tool.installation_path()) | |
| src_root = os.path.join(root, 'src') | |
| success = git_clone_checkout_and_pull(tool.url, src_root, tool.git_branch) | |
| if not success: | |
| return False | |
| build_dir = llvm_build_dir(tool) | |
| build_root = os.path.join(root, build_dir) | |
| build_type = decide_cmake_build_type(tool) | |
| # Configure | |
| cmake_generator, args = get_generator_and_config_args(tool) | |
| args += ['-DZSTD_FROM_INTERNET=ON'] | |
| cmakelists_dir = os.path.join(src_root) | |
| success = cmake_configure_and_build(cmake_generator, build_root, cmakelists_dir, build_type, args) | |
| if success: | |
| bin_dir = os.path.join(root, 'bin') | |
| mkdir_p(bin_dir) | |
| exe_paths = [os.path.join(build_root, 'Release', 'ccache'), os.path.join(build_root, 'ccache')] | |
| for e in exe_paths: | |
| for s in ['.exe', '']: | |
| ccache = e + s | |
| if os.path.isfile(ccache): | |
| dst = os.path.join(bin_dir, 'ccache' + s) | |
| shutil.copyfile(ccache, dst) | |
| os.chmod(dst, os.stat(dst).st_mode | stat.S_IEXEC) | |
| cache_dir = os.path.join(root, 'cache') | |
| write_file(os.path.join(root, 'emcc_ccache.conf'), f'''\ | |
| # Set maximum cache size to 10 GB: | |
| max_size = 10G | |
| cache_dir = {cache_dir} | |
| ''') | |
| mkdir_p(cache_dir) | |
| return success | |
| def download_chrome(tool): | |
| debug_print(f'download_chrome({tool})') | |
| # Is already downloaded? | |
| if tool.version not in {'stable', 'canary'} and is_chrome_installed(tool): | |
| print(f'Chrome {tool.version} already downloaded. Skipping.') | |
| return True | |
| # Use NPM puppeteer/browsers to install chrome. | |
| node_env, node_path = get_node_env() | |
| npx = os.path.join(os.path.abspath(node_path), 'npx' + ('.cmd' if WINDOWS else '')) | |
| print(f'Node: {npx}') | |
| cmd = [npx, '@puppeteer/browsers', 'install', f'chrome@{tool.version}'] | |
| print(str(cmd)) | |
| output = subprocess.check_output(cmd, env=node_env).decode('utf-8') | |
| chrome_path = output[output.find(' ') + 1:].strip() | |
| print(f'Chrome: {chrome_path}') | |
| # Create a pretend alias which maps the emsdk path to the actually installed version. | |
| pretend_version_dir = os.path.normpath(tool.installation_path()) | |
| actual_file = os.path.join(pretend_version_dir, 'actual.txt') | |
| os.makedirs(pretend_version_dir, exist_ok=True) | |
| write_file(actual_file, os.path.relpath(chrome_path, EMSDK_PATH)) | |
| return True | |
| def download_firefox(tool): | |
| debug_print(f'download_firefox({tool})') | |
| # Use mozdownload to acquire Firefox versions. | |
| try: | |
| from mozdownload import FactoryScraper | |
| except ImportError: | |
| # If mozdownload is not available, invoke pip to install it. | |
| subprocess.check_call([sys.executable, "-m", "pip", "install", "mozdownload"]) | |
| from mozdownload import FactoryScraper | |
| if WINDOWS: | |
| extension = 'exe' | |
| elif MACOS: | |
| extension = 'dmg' | |
| else: | |
| # N.b. on Linux even when we ask .tar.xz, we might sometimes get .tar.bz2, | |
| # depending on what is available on Firefox servers for the particular | |
| # version. So prepare to handle both further down below. | |
| extension = 'tar.xz' | |
| platform = None | |
| if LINUX and 'arm' in ARCH: | |
| platform = 'linux-arm64' | |
| if WINDOWS and 'arm' in ARCH: | |
| platform = 'win64-aarch64' | |
| if tool.version == 'nightly': | |
| scraper = FactoryScraper('daily', extension=extension, locale='en-US', platform=platform) | |
| else: | |
| scraper = FactoryScraper('release', extension=extension, locale='en-US', platform=platform, version=tool.version) | |
| if tool.version == 'nightly': | |
| firefox_version = os.path.basename(scraper.filename).split(".en-US")[0] | |
| else: | |
| firefox_version = os.path.basename(scraper.filename).split("firefox-")[1].split(".en-US")[0] | |
| print('Target Firefox version: ' + firefox_version) | |
| if tool.version in {'latest', 'latest-esr', 'latest-beta', 'nightly'}: | |
| pretend_version_dir = os.path.normpath(tool.installation_path()) | |
| orig_version = tool.version | |
| tool.version = firefox_version | |
| root = os.path.normpath(tool.installation_path()) | |
| tool.version = orig_version | |
| else: | |
| pretend_version_dir = None | |
| root = os.path.normpath(tool.installation_path()) | |
| def save_actual_version(): | |
| """For moving installer packages, e.g. "nightly", "latest", "latest-esr", | |
| store a text file to specify the actual installation directory. | |
| """ | |
| if os.path.isfile(firefox_exe) and pretend_version_dir: | |
| print(pretend_version_dir) | |
| os.makedirs(pretend_version_dir, exist_ok=True) | |
| write_file(os.path.join(pretend_version_dir, 'actual.txt'), os.path.relpath(root, EMSDK_PATH)) | |
| # Check if already installed | |
| print('Firefox installation root directory: ' + root) | |
| exe_dir = os.path.join(root, 'Contents', 'MacOS') if MACOS else root | |
| firefox_exe = os.path.join(exe_dir, exe_suffix('firefox')) | |
| if os.path.isfile(firefox_exe): | |
| print(f'{firefox_exe} is already installed, skipping..') | |
| save_actual_version() | |
| return True | |
| print('Downloading Firefox from ' + scraper.url) | |
| filename = scraper.download() | |
| print('Finished downloading ' + filename) | |
| if not MACOS: | |
| os.makedirs(root, exist_ok=True) | |
| if extension == 'exe': | |
| # Uncompress the NSIS installer to 'install' Firefox | |
| run(['C:\\Program Files\\7-Zip\\7z.exe', 'x', '-y', filename, '-o' + root]) | |
| if '.tar.' in filename: | |
| if filename.endswith('.tar.bz2'): | |
| tar_type = 'r:bz2' | |
| elif filename.endswith('.tar.xz'): | |
| tar_type = 'r:xz' | |
| else: | |
| raise Exception('Unknown archive type') | |
| with tarfile.open(filename, tar_type) as tar: | |
| tar.extractall(path=root) | |
| collapse_subdir = os.path.join(root, 'firefox') | |
| elif filename.endswith('.dmg'): | |
| mount_point = '/Volumes/Firefox Nightly' if tool.version == 'nightly' else '/Volumes/Firefox' | |
| app_name = 'Firefox Nightly.app' if tool.version == 'nightly' else 'Firefox.app' | |
| # If a previous mount point exists, detach it first | |
| if os.path.exists(mount_point): | |
| run(['hdiutil', 'detach', mount_point]) | |
| # Abort if detaching was not successful | |
| if os.path.exists(mount_point): | |
| raise Exception(f'Previous mount of Firefox already exists at "{mount_point}", unable to proceed.') | |
| # Mount the archive | |
| run(['hdiutil', 'attach', filename]) | |
| firefox_dir = os.path.join(mount_point, app_name) | |
| if not os.path.isdir(firefox_dir): | |
| raise Exception(f'Unable to find Firefox directory "{firefox_dir}" inside app image.') | |
| # And install by copying the files from the archive | |
| shutil.copytree(firefox_dir, root) | |
| run(['hdiutil', 'detach', mount_point]) | |
| collapse_subdir = None | |
| elif filename.endswith('.exe'): | |
| # NSIS installer package has a core/ directory, remove it as redundant. | |
| collapse_subdir = os.path.join(root, 'core') | |
| # Remove a redundant subdirectory by moving installed files up one directory. | |
| if collapse_subdir and os.path.isdir(collapse_subdir): | |
| # Rename the parent subdirectory first, since we will be handling a nested `firefox/firefox/` | |
| collapse = collapse_subdir + '_temp_renamed' | |
| os.rename(collapse_subdir, collapse) | |
| # Move all files up by one directory | |
| for f in os.listdir(collapse): | |
| shutil.move(os.path.join(collapse, f), os.path.dirname(collapse)) | |
| # The root directory should now be empty | |
| os.rmdir(collapse) | |
| # Original installer is now done. | |
| os.remove(filename) | |
| # Write a policy file that prevents Firefox from auto-updating itself. | |
| if MACOS: | |
| distribution_path = os.path.join(root, 'Contents', 'Resources', 'distribution') | |
| else: | |
| distribution_path = os.path.join(root, 'distribution') | |
| os.makedirs(distribution_path, exist_ok=True) | |
| write_file(os.path.join(distribution_path, 'policies.json'), '''\ | |
| { | |
| "policies": { | |
| "AppAutoUpdate": false, | |
| "DisableAppUpdate": true | |
| } | |
| }''') | |
| if MACOS: | |
| # Disable a macOS feature where if the browser is terminated mid-execution, (e.g. by | |
| # CI aborting), then the next time the browser is launched, macOS might bring up a dialog | |
| # "The last time you opened Firefox, it unexpectedly quit while reopening windows. | |
| # Do you want to try to reopen its windows again?" | |
| # that will block automated CI runs. | |
| # Disable this feature by changing the behavior of the program with macOS 'defaults'. | |
| run(['defaults', 'write', '-app', root, 'ApplePersistenceIgnoreState', 'YES']) | |
| run(['defaults', 'write', '-app', root, 'NSQuitAlwaysKeepsWindows', '-bool', 'false']) | |
| save_actual_version() | |
| # If we didn't get a Firefox executable, then installation failed. | |
| return os.path.isfile(firefox_exe) | |
| def is_chrome_installed(tool): | |
| actual_file = os.path.join(tool.installation_dir(), 'actual.txt') | |
| if not os.path.isfile(actual_file): | |
| return False | |
| chrome_exe = sdk_path(read_file(actual_file)) | |
| return os.path.isfile(chrome_exe) | |
| def is_firefox_installed(tool): | |
| actual_file = os.path.join(tool.installation_dir(), 'actual.txt') | |
| if not os.path.isfile(actual_file): | |
| return False | |
| actual_installation_dir = sdk_path(read_file(actual_file)) | |
| exe_dir = os.path.join(actual_installation_dir, 'Contents', 'MacOS') if MACOS else actual_installation_dir | |
| firefox_exe = os.path.join(exe_dir, exe_suffix('firefox')) | |
| return os.path.isfile(firefox_exe) | |
| def find_latest_installed_tool(name): | |
| """Finds the newest installed version of a given tool""" | |
| for t in reversed(tools): | |
| if t.id == name and t.is_installed(): | |
| return t | |
| def get_node_env(): | |
| node_tool = find_latest_installed_tool('node') | |
| if node_tool: | |
| node_path = node_tool.expand_vars(node_tool.activated_path) | |
| else: | |
| npm_fallback = shutil.which('npm') | |
| if not npm_fallback: | |
| errlog('Failed to find npm command') | |
| errlog('npm is required for Emscripten setup. Please install node.js first') | |
| return None, None | |
| node_path = os.path.dirname(npm_fallback) | |
| env = os.environ.copy() | |
| env["PATH"] = node_path + os.pathsep + env["PATH"] | |
| return env, node_path | |
| def emscripten_npm_setup(directory): | |
| env, node_path = get_node_env() | |
| if not env: | |
| return False | |
| npm = os.path.join(node_path, 'npm' + ('.cmd' if WINDOWS else '')) | |
| print('Running post-install step: npm ci ...') | |
| try: | |
| subprocess.check_call([npm, 'ci', '--production'], cwd=directory, env=env) | |
| except subprocess.CalledProcessError as e: | |
| errlog(f'Error running {e.cmd}') | |
| return False | |
| print('Done running: npm ci') | |
| return True | |
| def sdk_post_install(): | |
| """Older versions of the sdk did not include the node_modules directory | |
| and require `npm ci` to be run post-install | |
| """ | |
| emscripten_dir = os.path.join(EMSDK_PATH, 'upstream', 'emscripten') | |
| if os.path.exists(os.path.join(emscripten_dir, 'node_modules')): | |
| return True | |
| return emscripten_npm_setup(emscripten_dir) | |
| def emscripten_install(tool): | |
| """Modern versions of emscripten require bootstrap.py to be run before they | |
| can be used from a git checkout. On older versions we just run `npm ci` | |
| """ | |
| directory = tool.installation_path() | |
| if os.path.isfile(os.path.join(directory, 'bootstrap.py')): | |
| env = get_node_env()[0] | |
| if not env: | |
| return False | |
| try: | |
| subprocess.check_call([sys.executable, os.path.join(directory, 'bootstrap.py')], | |
| cwd=directory, stdin=subprocess.DEVNULL, env=env, text=True) | |
| except subprocess.CalledProcessError as e: | |
| errlog(f'Error running {e!s}') | |
| return False | |
| print('Done running: Emscripten bootstrap') | |
| else: | |
| return emscripten_npm_setup(directory) | |
| return True | |
| # Binaryen build scripts: | |
| def binaryen_build_root(tool): | |
| build_root = tool.installation_path().strip() | |
| if build_root.endswith(('/', '\\')): | |
| build_root = build_root[:-1] | |
| generator_prefix = cmake_generator_prefix() | |
| build_root = f'{build_root}{generator_prefix}_{tool.bitness}bit_binaryen' | |
| return build_root | |
| def uninstall_binaryen(tool): | |
| debug_print(f'uninstall_binaryen({tool})') | |
| build_root = binaryen_build_root(tool) | |
| print(f"Deleting path '{build_root}'") | |
| remove_tree(build_root) | |
| def is_binaryen_installed(tool): | |
| build_root = binaryen_build_root(tool) | |
| return os.path.exists(build_root) | |
| def build_binaryen_tool(tool): | |
| debug_print(f'build_binaryen_tool({tool})') | |
| src_root = tool.installation_path() | |
| build_root = binaryen_build_root(tool) | |
| build_type = decide_cmake_build_type(tool) | |
| # Configure | |
| cmake_generator, args = get_generator_and_config_args(tool) | |
| args += ['-DENABLE_WERROR=0'] # -Werror is not useful for end users | |
| args += ['-DBUILD_TESTS=0'] # We don't want to build or run tests | |
| if 'Visual Studio' in CMAKE_GENERATOR: | |
| if BUILD_FOR_TESTING: | |
| args += ['-DRUN_STATIC_ANALYZER=1'] | |
| success = cmake_configure_and_build(cmake_generator, build_root, src_root, build_type, args) | |
| if success: | |
| # Deploy scripts needed from source repository to build directory | |
| remove_tree(os.path.join(build_root, 'scripts')) | |
| shutil.copytree(os.path.join(src_root, 'scripts'), os.path.join(build_root, 'scripts')) | |
| remove_tree(os.path.join(build_root, 'src', 'js')) | |
| shutil.copytree(os.path.join(src_root, 'src', 'js'), os.path.join(build_root, 'src', 'js')) | |
| return success | |
| def download_and_extract(archive, dest_dir, filename_prefix='', clobber=True): | |
| debug_print(f'download_and_extract(archive={archive}, dest_dir={dest_dir})') | |
| url = urljoin(emsdk_packages_url, archive) | |
| def try_download(url): | |
| return download_file(url, download_dir, filename_prefix) | |
| # Special hack for the wasm-binaries we transitioned from `.bzip2` to | |
| # `.xz`, but we can't tell from the version/url which one to use, so | |
| # try one and then fall back to the other. | |
| success = False | |
| if 'wasm-binaries' in archive and os.path.splitext(archive)[1] == '.xz': | |
| success = try_download(url) | |
| if not success: | |
| alt_url = url.replace('.tar.xz', '.tbz2') | |
| success = try_download(alt_url) | |
| if success: | |
| url = alt_url | |
| if not success: | |
| success = try_download(url) | |
| if not success: | |
| return False | |
| # Remove the old directory, since we have some SDKs that install into the | |
| # same directory. If we didn't do this contents of the previous install | |
| # could remain. | |
| if clobber: | |
| remove_tree(dest_dir) | |
| download_target = get_download_target(url, download_dir, filename_prefix) | |
| if archive.endswith('.zip'): | |
| return unzip(download_target, dest_dir) | |
| else: | |
| return untargz(download_target, dest_dir) | |
| def to_native_path(p): | |
| if (WINDOWS and not MSYS) and not os_override: | |
| return to_unix_path(p).replace('/', '\\') | |
| else: | |
| return to_unix_path(p) | |
| def get_required_path(active_tools): | |
| """Finds and returns a list of the directories that need to be added to PATH for | |
| the given set of tools. | |
| """ | |
| path_add = [to_native_path(EMSDK_PATH)] | |
| for tool in active_tools: | |
| if tool.activated_path: | |
| path = to_native_path(tool.expand_vars(tool.activated_path)) | |
| # If the tool has an activated_path_skip attribute then we don't add | |
| # the tools path to the users path if a program by that name is found | |
| # in the existing PATH. This allows us to, for example, add our version | |
| # node to the users PATH if, and only if, they don't already have a | |
| # another version of node in their PATH. | |
| if tool.activated_path_skip: | |
| current_path = shutil.which(tool.activated_path_skip) | |
| # We found an executable by this name in the current PATH, but we | |
| # ignore our own version for this purpose. | |
| if current_path and os.path.dirname(current_path) != path: | |
| continue | |
| path_add.append(path) | |
| return path_add | |
| # Returns the absolute path to the file '.emscripten' for the current user on | |
| # this system. | |
| EM_CONFIG_PATH = os.path.join(EMSDK_PATH, ".emscripten") | |
| EM_CONFIG_DICT = {} | |
| def parse_key_value(line): | |
| if not line: | |
| return ('', '') | |
| eq = line.find('=') | |
| if eq != -1: | |
| key = line[0:eq].strip() | |
| value = line[eq + 1:].strip() | |
| return (key, value) | |
| else: | |
| return (key, '') | |
| def load_em_config(): | |
| EM_CONFIG_DICT.clear() | |
| lines = [] | |
| try: | |
| lines = read_file(EM_CONFIG_PATH).splitlines() | |
| except Exception: | |
| pass | |
| for line in lines: | |
| try: | |
| key, value = parse_key_value(line) | |
| if value: | |
| EM_CONFIG_DICT[key] = value | |
| except Exception: | |
| pass | |
| def find_emscripten_root(active_tools): | |
| """Find the currently active emscripten root. | |
| If there is more than one tool that defines EMSCRIPTEN_ROOT (this | |
| should not happen under normal circumstances), assume the last one takes | |
| precedence. | |
| """ | |
| root = None | |
| for tool in active_tools: | |
| config = tool.activated_config() | |
| if 'EMSCRIPTEN_ROOT' in config: | |
| root = config['EMSCRIPTEN_ROOT'] | |
| return root | |
| def fetch_nightly_node_versions(): | |
| url = "https://nodejs.org/download/nightly/" | |
| nightly_index_file = download_file(url, download_dir, filename_prefix='tmp_nodejs_nightly_index.html') | |
| if not nightly_index_file: | |
| debug_print('Unable to download Nightly Node.js versions!') | |
| return [] | |
| try: | |
| with open(nightly_index_file, encoding='utf-8') as f: | |
| html = f.read() | |
| finally: | |
| rmfile(nightly_index_file) | |
| # Regex to capture href values like v7.0.0-nightly2016080175c6d9dd95/ | |
| pattern = re.compile(r'<a href="(v[0-9]+\.[0-9]+\.[0-9]+-nightly[0-9a-f]+)/">') | |
| matches = pattern.findall(html) | |
| return matches | |
| def dir_installed_nightly_node_versions(): | |
| path = os.path.abspath('node') | |
| try: | |
| return [name for name in os.listdir(path) if os.path.isdir(os.path.join(path, name)) and name.startswith("nightly-")] | |
| except Exception: | |
| return [] | |
| def extract_newest_node_nightly_version(versions): | |
| def parse(v): | |
| # example: v7.0.0-nightly2016080175c6d9dd95 | |
| m = re.search(r'v(\d+)\.(\d+)\.(\d+)-nightly(\d{8})', v) | |
| if m: | |
| major, minor, patch, nightly = m.groups() | |
| return [int(major), int(minor), int(patch), int(nightly)] | |
| else: | |
| return [] | |
| try: | |
| return max(versions, key=parse) | |
| except Exception: | |
| return None | |
| def download_node_nightly(tool): | |
| nightly_versions = fetch_nightly_node_versions() | |
| latest_nightly = extract_newest_node_nightly_version(nightly_versions) | |
| print(f'Latest Node.js Nightly download available is "{latest_nightly}"') | |
| output_dir = os.path.abspath('node/nightly-' + latest_nightly) | |
| # Node.js zip structure quirk: Linux and macOS archives have a /bin, | |
| # Windows does not. Unify the file structures. | |
| if WINDOWS: | |
| output_dir += '/bin' | |
| if os.path.isdir(output_dir): | |
| return True | |
| url = tool.url.replace('%version%', latest_nightly) | |
| if WINDOWS: | |
| os_ = 'win' | |
| elif LINUX: | |
| os_ = 'linux' | |
| elif MACOS: | |
| os_ = 'darwin' | |
| else: | |
| os_ = '' | |
| if platform.machine().lower() in {'x86_64', 'amd64'}: | |
| arch = 'x64' | |
| elif platform.machine().lower() in {'arm64', 'aarch64'}: | |
| arch = 'arm64' | |
| if WINDOWS: | |
| zip_suffix = 'zip' | |
| else: | |
| zip_suffix = 'tar.gz' | |
| url = url.replace('%os%', os_) | |
| url = url.replace('%arch%', arch) | |
| url = url.replace('%zip_suffix%', zip_suffix) | |
| download_and_extract(url, output_dir) | |
| write_file(tool.get_version_file_path(), 'node-nightly-64bit') | |
| return True | |
| def get_emsdk_shell_env_configs(): | |
| """returns a tuple (string,string) of config files paths that need to used | |
| to activate emsdk env depending on $SHELL, defaults to bash. | |
| """ | |
| default_emsdk_env = sdk_path('emsdk_env.sh') | |
| default_shell_config_file = '$HOME/.bash_profile' | |
| shell = os.getenv('SHELL', '') | |
| if 'zsh' in shell: | |
| return (default_emsdk_env, '$HOME/.zprofile') | |
| elif 'csh' in shell: | |
| return (sdk_path('emsdk_env.csh'), '$HOME/.cshrc') | |
| elif 'fish' in shell: | |
| return (sdk_path('emsdk_env.fish'), '$HOME/.config/fish/config.fish') | |
| else: | |
| return (default_emsdk_env, default_shell_config_file) | |
| def generate_em_config(active_tools, permanently_activate, system): | |
| emroot = find_emscripten_root(active_tools) | |
| version = None | |
| if emroot and os.path.exists(os.path.join(emroot, 'emscripten-version.txt')): | |
| version = parse_emscripten_version(emroot) | |
| supports_cfgdir = version and version >= [6, 0, 4] | |
| cfg = '' | |
| if not supports_cfgdir: | |
| cfg += 'import os\n' | |
| cfg += "emsdk_path = os.path.dirname(os.getenv('EM_CONFIG')).replace('\\\\', '/')\n" | |
| # Different tools may provide the same activated configs; the latest to be | |
| # activated is the relevant one. | |
| activated_config = {} | |
| for tool in active_tools: | |
| activated_config.update(tool.activated_config()) | |
| if 'NODE_JS' not in activated_config: | |
| node_fallback = shutil.which('nodejs') | |
| if not node_fallback: | |
| node_fallback = 'node' | |
| activated_config['NODE_JS'] = node_fallback | |
| for name, value in activated_config.items(): | |
| if value.startswith('['): | |
| cfg += f'{name} = {value}\n' | |
| else: | |
| cfg += f"{name} = '{value}'\n" | |
| if version: | |
| # Older emscripten versions of emscripten depend on certain config | |
| # keys that are no longer used. | |
| # See https://github.com/emscripten-core/emscripten/pull/9469 | |
| if version < [1, 38, 46]: | |
| cfg += 'COMPILER_ENGINE = NODE_JS\n' | |
| # See https://github.com/emscripten-core/emscripten/pull/9542 | |
| if version < [1, 38, 48]: | |
| cfg += 'JS_ENGINES = [NODE_JS]\n' | |
| if supports_cfgdir: | |
| cfg = cfg.replace(EMSDK_PATH, '$CFGDIR') | |
| else: | |
| cfg = cfg.replace("'" + EMSDK_PATH, "emsdk_path + '") | |
| if os.path.exists(EM_CONFIG_PATH): | |
| backup_path = EM_CONFIG_PATH + ".old" | |
| move_with_overwrite(EM_CONFIG_PATH, backup_path) | |
| write_file(EM_CONFIG_PATH, cfg) | |
| # Clear old emscripten content. | |
| rmfile(os.path.join(EMSDK_PATH, ".emscripten_sanity")) | |
| path_add = get_required_path(active_tools) | |
| # Give some recommended next step, depending on the platform | |
| if WINDOWS: | |
| if not permanently_activate and not system: | |
| print('Next steps:') | |
| print('- Consider running `emsdk activate` with --permanent or --system') | |
| print(' to have emsdk settings available on startup.') | |
| else: | |
| print('Next steps:') | |
| print('- To conveniently access emsdk tools from the command line,') | |
| print(' consider adding the following directories to your PATH:') | |
| for p in path_add: | |
| print(' ' + p) | |
| print('- This can be done for the current shell by running:') | |
| emsdk_env, shell_config_file = get_emsdk_shell_env_configs() | |
| print(f' source "{emsdk_env}"') | |
| print('- Configure emsdk in your shell startup scripts by running:') | |
| print(f' echo \'source "{emsdk_env}"\' >> {shell_config_file}') | |
| class Tool: | |
| os = None | |
| bitness = None | |
| append_bitness = True | |
| is_old = False | |
| version = None | |
| activated_path = None | |
| cmake_build_type = None | |
| install_path = None | |
| activated_path_skip = False | |
| activated_cfg = None | |
| activated_env = None | |
| arch = None | |
| custom_is_installed_script = None | |
| custom_install_script = None | |
| custom_uninstall_script = None | |
| emscripten_releases_hash = None | |
| git_branch = None | |
| url = None | |
| def __init__(self, data): | |
| self.deps = [] | |
| # Convert the dictionary representation of the tool in 'data' to members of | |
| # this class. Base attributes are assigned first, then host OS-matching suffixed | |
| # attributes (e.g. url_windows, activated_path_windows) are applied on top as overrides. | |
| # Foreign OS keys are ignored. | |
| os_overrides = {} | |
| all_os_names = {'windows', 'macos', 'linux'} | |
| for key, value in data.items(): | |
| if '_' in key: | |
| prefix, suffix = key.rsplit('_', 1) | |
| if suffix in all_os_names: | |
| if suffix == os_name(): | |
| os_overrides[prefix] = value | |
| continue | |
| setattr(self, key, value) | |
| for prefix, value in os_overrides.items(): | |
| setattr(self, prefix, value) | |
| # Cache the name ID of this Tool (these are read very often) | |
| self.name = self.id | |
| if self.version: | |
| self.name += f'-{self.version}' | |
| if self.bitness: | |
| self.name += f'-{self.bitness}bit' | |
| def __str__(self): | |
| return self.name | |
| def expand_vars(self, str): | |
| if '%installation_dir%' in str: | |
| str = str.replace('%installation_dir%', sdk_path(self.installation_dir())) | |
| if '%macos_app_bundle_prefix%' in str: | |
| str = str.replace('%macos_app_bundle_prefix%', 'Contents/MacOS/' if MACOS else '') | |
| if '%actual_installation_dir%' in str: | |
| actual_file = os.path.join(self.installation_dir(), 'actual.txt') | |
| if os.path.isfile(actual_file): | |
| str = str.replace('%actual_installation_dir%', sdk_path(read_file(actual_file))) | |
| else: | |
| str = str.replace('%actual_installation_dir%', '__NOT_INSTALLED__') | |
| if '%generator_prefix%' in str: | |
| str = str.replace('%generator_prefix%', cmake_generator_prefix()) | |
| str = str.replace('%.exe%', '.exe' if WINDOWS else '') | |
| if '%llvm_build_bin_dir%' in str: | |
| str = str.replace('%llvm_build_bin_dir%', llvm_build_bin_dir(self)) | |
| if '%latest_downloaded_node_nightly_dir%' in str: | |
| installed_node_nightlys = dir_installed_nightly_node_versions() | |
| latest_node_nightly = extract_newest_node_nightly_version(installed_node_nightlys) | |
| if latest_node_nightly: | |
| str = str.replace('%latest_downloaded_node_nightly_dir%', latest_node_nightly) | |
| return str | |
| def needs_compilation(self): | |
| """Return true if this tool requires building from source, and false if this is a precompiled tool.""" | |
| if self.cmake_build_type: | |
| return True | |
| return any(dep.needs_compilation() for dep in self.dependencies()) | |
| def installation_path(self): | |
| """Specifies the target path where this tool will be installed to. This could | |
| either be a directory or a filename (e.g. in case of node.js) | |
| """ | |
| if self.install_path: | |
| pth = self.expand_vars(self.install_path) | |
| return sdk_path(pth) | |
| p = self.version | |
| if self.bitness and self.append_bitness: | |
| p += f'_{self.bitness}bit' | |
| return sdk_path(os.path.join(self.id, p)) | |
| def installation_dir(self): | |
| """Specifies the target directory this tool will be installed to.""" | |
| dir = self.installation_path() | |
| if path_points_to_directory(dir): | |
| return dir | |
| else: | |
| return os.path.dirname(dir) | |
| def activated_config(self): | |
| """Returns the configuration item that needs to be added to .emscripten to make | |
| this Tool active for the current user. | |
| """ | |
| if not self.activated_cfg: | |
| return {} | |
| config = {} | |
| expanded = to_unix_path(self.expand_vars(self.activated_cfg)) | |
| for specific_cfg in expanded.split(';'): | |
| name, value = specific_cfg.split('=') | |
| config[name] = value.strip("'") | |
| return config | |
| def activated_environment(self): | |
| if not self.activated_env: | |
| return [] | |
| return self.expand_vars(self.activated_env).split(';') | |
| def compatible_with_this_arch(self): | |
| return self.arch == ARCH or not self.arch | |
| def compatible_with_this_os(self): | |
| if not self.compatible_with_this_arch(): | |
| return False | |
| if self.os: | |
| assert self.os in {'linux', 'windows', 'macos'} | |
| return self.os == os_name() | |
| # Tools without an explicit 'os' are compatible with the current | |
| # OS if they have URL for the current OS *or* a set of dependencies | |
| # for the current OS. | |
| return self.url is not None or bool(self.deps) | |
| def get_version_file_path(self): | |
| """the "version file" is a file inside install dirs that indicates the | |
| version installed there. this helps disambiguate when there is more than | |
| one version that may be installed to the same directory (which is used | |
| to avoid accumulating builds over time in some cases, with new builds | |
| overwriting the old) | |
| """ | |
| return os.path.join(self.installation_path(), '.emsdk_version') | |
| def is_installed_version(self): | |
| version_file_path = self.get_version_file_path() | |
| if os.path.isfile(version_file_path): | |
| return read_file(version_file_path).strip() == self.name | |
| return False | |
| def update_installed_version(self): | |
| write_file(self.get_version_file_path(), self.name + '\n') | |
| def is_installed(self, skip_version_check=False): | |
| # If this tool/sdk depends on other tools, require that all dependencies are | |
| # installed for this tool to count as being installed. | |
| for dep in self.dependencies(): | |
| if not dep.is_installed(): | |
| return False | |
| if self.url is None: | |
| debug_print(f'{self} has no files to download, so is installed by default.') | |
| return True | |
| content_exists = is_nonempty_directory(self.installation_path()) | |
| debug_print(f'{self} installation path is {self.installation_path()}, exists: {content_exists}.') | |
| # For e.g. fastcomp clang from git repo, the activated PATH is the | |
| # directory where the compiler is built to, and installation_path is | |
| # the directory where the source tree exists. To distinguish between | |
| # multiple packages sharing the same source (clang-main-32bit, | |
| # clang-main-64bit, clang-main-32bit and clang-main-64bit each | |
| # share the same git repo), require that in addition to the installation | |
| # directory, each item in the activated PATH must exist. | |
| if self.activated_path and not os.path.exists(self.expand_vars(self.activated_path)): | |
| content_exists = False | |
| if self.custom_is_installed_script: | |
| if self.custom_is_installed_script == 'is_binaryen_installed': | |
| return is_binaryen_installed(self) | |
| elif self.custom_is_installed_script == 'is_firefox_installed': | |
| return is_firefox_installed(self) | |
| elif self.custom_is_installed_script == 'is_chrome_installed': | |
| return is_chrome_installed(self) | |
| else: | |
| raise Exception(f'Unknown custom_is_installed_script: "{self.custom_is_installed_script}"') | |
| return content_exists and (skip_version_check or self.is_installed_version()) | |
| def is_active(self): | |
| if not self.is_installed(): | |
| return False | |
| # All dependencies of this tool must be active as well. | |
| deps = self.dependencies() | |
| for tool in deps: | |
| if not tool.is_active(): | |
| return False | |
| activated_cfg = self.activated_config() | |
| if not activated_cfg: | |
| return len(deps) > 0 | |
| for key, value in activated_cfg.items(): | |
| if key not in EM_CONFIG_DICT: | |
| debug_print(f'{self} is not active, because key="{key}" does not exist in .emscripten') | |
| return False | |
| # all paths are stored dynamically relative to the emsdk root, so | |
| # normalize those first. | |
| config_value = EM_CONFIG_DICT[key].replace("emsdk_path + '", "'" + EMSDK_PATH).replace("$CFGDIR", EMSDK_PATH) | |
| config_value = config_value.strip("'") | |
| if config_value != value: | |
| debug_print(f'{self} is not active, because key="{key}" has value "{config_value}" but should have value "{value}"') | |
| return False | |
| return True | |
| def is_env_active(self): | |
| """Returns true if the system environment variables requires by this tool are currently active.""" | |
| envs = self.activated_environment() | |
| for env in envs: | |
| key, value = parse_key_value(env) | |
| if key not in os.environ or to_unix_path(os.environ[key]) != to_unix_path(value): | |
| debug_print(f'{self} is not active, because environment variable key="{key}" has value "{os.getenv(key)}" but should have value "{value}"') | |
| return False | |
| if self.activated_path: | |
| path = to_unix_path(self.expand_vars(self.activated_path)) | |
| for p in path: | |
| path_items = os.environ['PATH'].replace('\\', '/').split(ENVPATH_SEPARATOR) | |
| if not normalized_contains(path_items, p): | |
| debug_print(f'{self} is not active, because environment variable PATH item "{p}" is not present (PATH={os.environ["PATH"]})') | |
| return False | |
| return True | |
| def can_be_installed(self): | |
| """If this tool can be installed on this system, this function returns True. | |
| Otherwise, this function returns a string that describes the reason why this | |
| tool is not available. | |
| """ | |
| if self.bitness == 64 and not is_os_64bit(): | |
| return "this tool is only provided for 64-bit OSes" | |
| return True | |
| def install(self): | |
| """Returns True if the Tool was installed of False if was skipped due to | |
| already being installed. | |
| """ | |
| if self.can_be_installed() is not True: | |
| exit_with_error(f"The tool '{self}' is not available due to the reason: {self.can_be_installed()}") | |
| if self.id == 'sdk': | |
| return self.install_sdk() | |
| else: | |
| return self.install_tool() | |
| def install_sdk(self): | |
| """Returns True if any SDK component was installed of False all componented | |
| were already installed. | |
| """ | |
| print(f"Installing SDK '{self}'..") | |
| installed = False | |
| for dep in self.dependencies(): | |
| installed |= dep.install() | |
| if not installed: | |
| print(f"All SDK components already installed: '{self}'.") | |
| return False | |
| if self.custom_install_script == 'sdk_post_install': | |
| if not sdk_post_install(): | |
| exit_with_error('post-install step failed: sdk_post_install') | |
| print(f"Done installing SDK '{self}'.") | |
| return True | |
| def install_tool(self): | |
| """Returns True if the SDK was installed of False if was skipped due to | |
| already being installed. | |
| """ | |
| # Avoid doing a redundant reinstall of the tool, if it has already been installed. | |
| # However all tools that are sourced directly from git branches do need to be | |
| # installed every time when requested, since the install step is then used to git | |
| # pull the tool to a newer version. | |
| if self.is_installed() and not self.git_branch: | |
| print(f"Skipped installing {self.name}, already installed.") | |
| return False | |
| print(f"Installing tool '{self}'..") | |
| custom_install_scripts = { | |
| 'build_llvm': build_llvm, | |
| 'build_ninja': build_ninja, | |
| 'build_ccache': build_ccache, | |
| 'download_node_nightly': download_node_nightly, | |
| 'download_firefox': download_firefox, | |
| 'download_chrome': download_chrome, | |
| } | |
| if self.custom_install_script in custom_install_scripts: | |
| success = custom_install_scripts[self.custom_install_script](self) | |
| elif self.git_branch: | |
| success = git_clone_checkout_and_pull(self.url, self.installation_path(), self.git_branch, getattr(self, 'remote_name', 'origin')) | |
| elif self.url.endswith(ARCHIVE_SUFFIXES): | |
| success = download_and_extract(self.url, self.installation_path(), | |
| filename_prefix=getattr(self, 'download_prefix', '')) | |
| else: | |
| assert False, 'unhandled url type: ' + self.url | |
| if not success: | |
| exit_with_error("installation failed!") | |
| if self.custom_install_script: | |
| if self.custom_install_script == 'emscripten_install': | |
| success = emscripten_install(self) | |
| elif self.custom_install_script in {'build_llvm', 'build_ninja', 'build_ccache', 'download_node_nightly', 'download_firefox', 'download_chrome'}: | |
| # 'build_llvm' is a special one that does the download on its | |
| # own, others do the download manually. | |
| pass | |
| elif self.custom_install_script == 'build_binaryen': | |
| success = build_binaryen_tool(self) | |
| else: | |
| raise Exception(f'Unknown custom_install_script: "{self.custom_install_script}"') | |
| if not success: | |
| exit_with_error("installation failed!") | |
| # Install an emscripten-version.txt file if told to, and if there is one. | |
| # (If this is not an actual release, but some other build, then we do not | |
| # write anything.) | |
| if self.emscripten_releases_hash: | |
| emscripten_version_file_path = os.path.join(to_native_path(self.expand_vars(self.activated_path)), 'emscripten-version.txt') | |
| version = get_emscripten_release_version(self.emscripten_releases_hash) | |
| if version: | |
| write_file(emscripten_version_file_path, f'"{version}"\n') | |
| print(f"Done installing tool '{self}'.") | |
| # Sanity check that the installation succeeded, and if so, remove unneeded | |
| # leftover installation files. | |
| if not self.is_installed(skip_version_check=True): | |
| 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.") | |
| self.cleanup_temp_install_files() | |
| self.update_installed_version() | |
| return True | |
| def cleanup_temp_install_files(self): | |
| if KEEP_DOWNLOADS: | |
| return | |
| if self.url.endswith(ARCHIVE_SUFFIXES): | |
| download_target = get_download_target(self.url, download_dir, getattr(self, 'download_prefix', '')) | |
| debug_print(f"Deleting temporary download: {download_target}") | |
| rmfile(download_target) | |
| def uninstall(self): | |
| if not self.is_installed(): | |
| print(f"Tool '{self}' was not installed. No need to uninstall.") | |
| return | |
| print(f"Uninstalling tool '{self}'..") | |
| if self.custom_uninstall_script: | |
| if self.custom_uninstall_script == 'uninstall_binaryen': | |
| uninstall_binaryen(self) | |
| else: | |
| raise Exception(f'Unknown custom_uninstall_script directive "{self.custom_uninstall_script}"!') | |
| print(f"Deleting path '{self.installation_path()}'") | |
| remove_tree(self.installation_path()) | |
| print(f"Done uninstalling '{self}'.") | |
| def dependencies(self): | |
| deps = [] | |
| for dep in self.deps: | |
| tool = find_tool(dep) | |
| assert tool, f"dependency '{dep}' of '{self}' not found" | |
| deps += [tool] | |
| return deps | |
| def recursive_dependencies(self): | |
| deps = [] | |
| for dep in self.deps: | |
| tool = find_tool(dep) | |
| assert tool, f"dependency '{dep}' of '{self}' not found" | |
| deps += [tool] | |
| deps += tool.recursive_dependencies() | |
| return deps | |
| # A global registry of all known Emscripten SDK tools available in the SDK manifest. | |
| tools = [] | |
| tools_map = {} | |
| def add_tool(tool): | |
| tool.is_sdk = False | |
| existing = find_tool(str(tool)) | |
| if existing: | |
| raise Exception(f'Duplicate tool {tool}! Existing:\n{vars(existing)}, New:\n{vars(tool)}') | |
| tools.append(tool) | |
| tools_map[str(tool)] = tool | |
| # A global registry of all known SDK toolsets. | |
| sdks = [] | |
| sdks_map = {} | |
| def add_sdk(sdk): | |
| sdk.is_sdk = True | |
| existing = find_sdk(str(sdk)) | |
| if existing: | |
| raise Exception(f'Duplicate sdk {sdk}! Existing:\n{vars(existing)}, New:\n{vars(sdk)}') | |
| sdks.append(sdk) | |
| sdks_map[str(sdk)] = sdk | |
| # N.B. In both tools and sdks list above, we take the convention that the newest | |
| # items are at the back of the list (ascending chronological order) | |
| def find_tool(name): | |
| return tools_map.get(name) | |
| def find_sdk(name): | |
| return sdks_map.get(name) | |
| def is_os_64bit(): | |
| return ARCH.endswith('64') | |
| def find_latest_version(): | |
| return resolve_sdk_aliases('latest') | |
| def find_latest_hash(): | |
| version = find_latest_version() | |
| releases_info = load_releases_info() | |
| return releases_info['releases'][version] | |
| def resolve_sdk_aliases(name, verbose=False): | |
| releases_info = load_releases_info() | |
| while name in releases_info['aliases']: | |
| if verbose: | |
| print(f"Resolving SDK alias '{name}' to '{releases_info['aliases'][name]}'") | |
| name = releases_info['aliases'][name] | |
| return name | |
| def find_latest_sdk(): | |
| return f'sdk-releases-{find_latest_hash()}-64bit' | |
| def find_tot_sdk(): | |
| debug_print('Fetching emscripten-releases repository...') | |
| global extra_release_tag | |
| extra_release_tag = get_emscripten_releases_tot() | |
| return f'sdk-releases-{extra_release_tag}-64bit' | |
| def parse_emscripten_version(emscripten_root): | |
| version_file = os.path.join(emscripten_root, 'emscripten-version.txt') | |
| version = read_file(version_file).strip() | |
| version = version.strip('"') | |
| suffix = None | |
| if '-' in version: | |
| version, suffix = version.split('-') | |
| version = [int(v) for v in version.split('.')] | |
| if suffix == 'git': | |
| # Treat 1.2.3-git as 1.2.2 since it doesn't contain all the changes | |
| # present in the final 1.2.3 release. | |
| version[-1] -= 1 | |
| return version | |
| def get_emscripten_release_version(emscripten_releases_hash): | |
| """Given a git hash in emscripten-releases, find the emscripten | |
| version for it. There may not be one if this is not the hash of | |
| a release, in which case we return None. | |
| """ | |
| releases_info = load_releases_info() | |
| for key, value in dict(releases_info['releases']).items(): | |
| if value == emscripten_releases_hash: | |
| return key.split('-')[0] | |
| return None | |
| def get_emscripten_releases_tot(): | |
| """Get the tip-of-tree build identifier.""" | |
| success = git_clone_checkout_and_pull(emscripten_releases_repo, sdk_path('releases'), 'main') | |
| if not success: | |
| exit_with_error('error checking out emscripten-releases repo') | |
| recent_releases = git_recent_commits(sdk_path('releases')) | |
| # The recent releases are the latest hashes in the git repo. There | |
| # may not be a build for the most recent ones yet; find the last | |
| # that has uploaded binaries for all platforms. | |
| all_platforms = [ | |
| ('linux', '', 'tar.xz'), | |
| ('linux', '-arm64', 'tar.xz'), | |
| ('mac', '', 'tar.xz'), | |
| ('mac', '-arm64', 'tar.xz'), | |
| ('win', '', 'zip'), | |
| ] | |
| def check_binary(release, os_name, arch, ext): | |
| url = emscripten_releases_download_url_template % (os_name, release, arch, ext) | |
| try: | |
| urlopen(url) | |
| return True | |
| except Exception: | |
| return False | |
| for release in recent_releases: | |
| if all(check_binary(release, os_name, arch, ext) for os_name, arch, ext in all_platforms): | |
| return release | |
| # No recent release has binaries for all platforms | |
| exit_with_error('failed to find build of any recent emsdk revision') | |
| def get_release_hash(arg, releases_info): | |
| return releases_info.get(arg, None) or releases_info.get(f'sdk-{arg}-64bit') | |
| def version_key(ver): | |
| return tuple(map(int, re.split('[._-]', ver)[:3])) | |
| def is_emsdk_sourced_from_github(): | |
| return os.path.exists(os.path.join(EMSDK_PATH, '.git')) | |
| def update_emsdk(): | |
| if is_emsdk_sourced_from_github(): | |
| 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)') | |
| sys.exit(1) | |
| if not download_and_extract(emsdk_zip_download_url, EMSDK_PATH, clobber=False): | |
| sys.exit(1) | |
| def load_legacy_emscripten_tags(): | |
| """Lists all legacy (pre-emscripten-releases) tagged versions directly in the Git | |
| repositories. These we can pull and compile from source. | |
| """ | |
| return read_file(sdk_path('legacy-emscripten-tags.txt')).splitlines() | |
| def load_legacy_binaryen_tags(): | |
| return read_file(sdk_path('legacy-binaryen-tags.txt')).splitlines() | |
| def remove_prefix(s, prefix): | |
| if s.startswith(prefix): | |
| return s[len(prefix):] | |
| else: | |
| return s | |
| def remove_suffix(s, suffix): | |
| if s.endswith(suffix): | |
| return s[:len(s) - len(suffix)] | |
| else: | |
| return s | |
| def load_file_index_list(filename): | |
| """filename should be one of: 'llvm-precompiled-tags-32bit.txt', 'llvm-precompiled-tags-64bit.txt'""" | |
| items = read_file(sdk_path(filename)).splitlines() | |
| items = [remove_suffix(remove_suffix(remove_prefix(x, 'emscripten-llvm-e'), '.tar.gz'), '.zip').strip() for x in items] | |
| items = [x for x in items if 'latest' not in x and len(x) > 0] | |
| # Sort versions from oldest to newest (the default sort would be | |
| # lexicographic, i.e. '1.37.1 < 1.37.10 < 1.37.2') | |
| return sorted(items, key=version_key) | |
| def load_releases_info(): | |
| """Load the json info for emscripten-releases.""" | |
| if not hasattr(load_releases_info, 'cached_info'): | |
| try: | |
| text = read_file(sdk_path('emscripten-releases-tags.json')) | |
| load_releases_info.cached_info = json.loads(text) | |
| except Exception as e: | |
| print('Error parsing emscripten-releases-tags.json!') | |
| exit_with_error(str(e)) | |
| return load_releases_info.cached_info | |
| def get_installed_sdk_version(): | |
| version_file = sdk_path(os.path.join('upstream', '.emsdk_version')) | |
| if not os.path.exists(version_file): | |
| return None | |
| version = read_file(version_file) | |
| return version.split('-')[1] | |
| def load_releases_tags(): | |
| """Get a list of tags for emscripten-releases.""" | |
| tags = [] | |
| info = load_releases_info() | |
| for _version, sha in sorted(info['releases'].items(), key=lambda x: version_key(x[0])): | |
| tags.append(sha) | |
| if extra_release_tag: | |
| tags.append(extra_release_tag) | |
| # Explicitly add the currently installed SDK version. This could be a custom | |
| # version (installed explicitly) so it might not be part of the main list | |
| # loaded above. | |
| installed = get_installed_sdk_version() | |
| if installed and installed not in tags: | |
| tags.append(installed) | |
| return tags | |
| def load_releases_versions(): | |
| info = load_releases_info() | |
| versions = list(info['releases'].keys()) | |
| return versions | |
| def load_sdk_manifest(): | |
| try: | |
| manifest = json.loads(read_file(sdk_path('emsdk_manifest.json'))) | |
| except Exception: | |
| errlog('Error parsing emsdk_manifest.json!') | |
| raise | |
| emscripten_tags = load_legacy_emscripten_tags() | |
| llvm_precompiled_tags_32bit = [] | |
| llvm_precompiled_tags_64bit = load_file_index_list('llvm-tags-64bit.txt') | |
| llvm_precompiled_tags = llvm_precompiled_tags_32bit + llvm_precompiled_tags_64bit | |
| binaryen_tags = load_legacy_binaryen_tags() | |
| releases_tags = load_releases_tags() | |
| def dependencies_exist(sdk): | |
| for dep in sdk.deps: | |
| tool = find_tool(dep) | |
| if not tool: | |
| debug_print('missing dependency: ' + dep) | |
| return False | |
| return True | |
| def cmp_version(ver, cmp_operand, reference): | |
| if cmp_operand == '<=': | |
| return version_key(ver) <= version_key(reference) | |
| if cmp_operand == '<': | |
| return version_key(ver) < version_key(reference) | |
| if cmp_operand == '>=': | |
| return version_key(ver) >= version_key(reference) | |
| if cmp_operand == '>': | |
| return version_key(ver) > version_key(reference) | |
| if cmp_operand == '==': | |
| return version_key(ver) == version_key(reference) | |
| if cmp_operand == '!=': | |
| return version_key(ver) != version_key(reference) | |
| raise Exception(f'Invalid cmp_operand "{cmp_operand}"!') | |
| def passes_filters(param, ver, filters): | |
| for v in filters: | |
| if v[0] == param and not cmp_version(ver, v[1], v[2]): | |
| return False | |
| return True | |
| def expand_category_param(param, category_list, t, is_sdk): | |
| """A 'category parameter' is a %foo%-encoded identifier that specifies | |
| a class of tools instead of just one tool, e.g. %tag% | |
| """ | |
| for i, ver in enumerate(category_list): | |
| if not ver.strip(): | |
| continue | |
| t2 = copy.copy(t) | |
| found_param = False | |
| for p, v in vars(t2).items(): | |
| if isinstance(v, str) and param in v: | |
| t2.__dict__[p] = v.replace(param, ver) | |
| found_param = True | |
| if not found_param: | |
| continue | |
| t2.is_old = i < len(category_list) - 2 | |
| t2.deps = [x.replace(param, ver) for x in t2.deps] | |
| if is_sdk: | |
| if dependencies_exist(t2): | |
| if not find_sdk(t2.name): | |
| add_sdk(t2) | |
| else: | |
| debug_print(f'SDK {t2} already existed in manifest, not adding twice') | |
| else: | |
| if not find_tool(t2.name): | |
| add_tool(t2) | |
| else: | |
| debug_print(f'Tool {t2} already existed in manifest, not adding twice') | |
| for tool in manifest['tools']: | |
| t = Tool(tool) | |
| if t.compatible_with_this_os(): | |
| # Expand the metapackages that refer to tags | |
| if '%tag%' in t.version: | |
| expand_category_param('%tag%', emscripten_tags, t, is_sdk=False) | |
| elif '%precompiled_tag%' in t.version: | |
| expand_category_param('%precompiled_tag%', llvm_precompiled_tags, t, is_sdk=False) | |
| elif '%precompiled_tag32%' in t.version: | |
| expand_category_param('%precompiled_tag32%', llvm_precompiled_tags_32bit, t, is_sdk=False) | |
| elif '%precompiled_tag64%' in t.version: | |
| expand_category_param('%precompiled_tag64%', llvm_precompiled_tags_64bit, t, is_sdk=False) | |
| elif '%binaryen_tag%' in t.version: | |
| expand_category_param('%binaryen_tag%', binaryen_tags, t, is_sdk=False) | |
| elif '%releases-tag%' in t.version: | |
| expand_category_param('%releases-tag%', releases_tags, t, is_sdk=False) | |
| else: | |
| add_tool(t) | |
| for sdk_str in manifest['sdks']: | |
| sdk_str['id'] = 'sdk' | |
| sdk = Tool(sdk_str) | |
| if sdk.compatible_with_this_os(): | |
| if not hasattr(sdk, 'is_old'): | |
| sdk.is_old = False | |
| if '%tag%' in sdk.version: | |
| expand_category_param('%tag%', emscripten_tags, sdk, is_sdk=True) | |
| elif '%precompiled_tag%' in sdk.version: | |
| expand_category_param('%precompiled_tag%', llvm_precompiled_tags, sdk, is_sdk=True) | |
| elif '%precompiled_tag32%' in sdk.version: | |
| expand_category_param('%precompiled_tag32%', llvm_precompiled_tags_32bit, sdk, is_sdk=True) | |
| elif '%precompiled_tag64%' in sdk.version: | |
| expand_category_param('%precompiled_tag64%', llvm_precompiled_tags_64bit, sdk, is_sdk=True) | |
| elif '%releases-tag%' in sdk.version: | |
| expand_category_param('%releases-tag%', releases_tags, sdk, is_sdk=True) | |
| else: | |
| add_sdk(sdk) | |
| def can_simultaneously_activate(tool1, tool2): | |
| """Tests if the two given tools can be active at the same time. | |
| Currently only a simple check for name for same tool with different versions, | |
| possibly adds more logic in the future. | |
| """ | |
| return tool1.id != tool2.id | |
| def process_tool_list(tools_to_activate): | |
| """Expands dependencies for each tool, and removes ones that don't exist.""" | |
| i = 0 | |
| # Gather dependencies for each tool | |
| while i < len(tools_to_activate): | |
| tool = tools_to_activate[i] | |
| deps = tool.recursive_dependencies() | |
| tools_to_activate = tools_to_activate[:i] + deps + tools_to_activate[i:] | |
| i += len(deps) + 1 | |
| for tool in tools_to_activate: | |
| if not tool.is_installed(): | |
| exit_with_error(f"error: tool is not installed and therefore cannot be activated: '{tool}'") | |
| # Remove conflicting tools | |
| i = 0 | |
| while i < len(tools_to_activate): | |
| j = 0 | |
| while j < i: | |
| secondary_tool = tools_to_activate[j] | |
| primary_tool = tools_to_activate[i] | |
| if not can_simultaneously_activate(primary_tool, secondary_tool): | |
| tools_to_activate.pop(j) | |
| j -= 1 | |
| i -= 1 | |
| j += 1 | |
| i += 1 | |
| return tools_to_activate | |
| def write_set_env_script(env_string): | |
| assert CMD or POWERSHELL | |
| write_file(EMSDK_SET_ENV, env_string) | |
| def set_active_tools(tools_to_activate, permanently_activate, system): | |
| """Reconfigure .emscripten to choose the currently activated toolset, set PATH | |
| and other environment variables. | |
| Returns the full list of deduced tools that are now active. | |
| """ | |
| tools_to_activate = process_tool_list(tools_to_activate) | |
| if tools_to_activate: | |
| tools = [x for x in tools_to_activate if not x.is_sdk] | |
| print('Setting the following tools as active:') | |
| for t in tools: | |
| print(f' {t}') | |
| print('') | |
| generate_em_config(tools_to_activate, permanently_activate, system) | |
| # Construct a .bat or .ps1 script that will be invoked to set env. vars and PATH | |
| # We only do this on cmd or powershell since emsdk.bat/ps1 is able to modify the | |
| # calling shell environment. On other shell `source emsdk_env.sh` is | |
| # required. | |
| if CMD or POWERSHELL: | |
| # always set local environment variables since permanently activating will only set the registry settings and | |
| # will not affect the current session | |
| env_vars_to_add = get_env_vars_to_add(tools_to_activate, system, user=permanently_activate) | |
| env_string = construct_env_with_vars(env_vars_to_add) | |
| write_set_env_script(env_string) | |
| if WINDOWS and permanently_activate: | |
| win_set_environment_variables(env_vars_to_add, system, user=permanently_activate) | |
| return tools_to_activate | |
| def currently_active_sdk(): | |
| for sdk in reversed(sdks): | |
| if sdk.is_active(): | |
| return sdk | |
| return None | |
| def currently_active_tools(): | |
| active_tools = [] | |
| for tool in tools: | |
| if tool.is_active(): | |
| active_tools += [tool] | |
| return active_tools | |
| # http://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-in-python-whilst-preserving-order | |
| def unique_items(seq): | |
| seen = set() | |
| seen_add = seen.add | |
| return [x for x in seq if x not in seen and not seen_add(x)] | |
| def normalized_contains(lst, elem): | |
| """Tests if a path is contained in the given list, but with separators normalized.""" | |
| elem = to_unix_path(elem) | |
| for e in lst: | |
| if elem == to_unix_path(e): | |
| return True | |
| return False | |
| def to_msys_path(p): | |
| p = to_unix_path(p) | |
| new_path = re.sub(r'([a-zA-Z]):/(.*)', r'/\1/\2', p) | |
| if len(new_path) > 3 and new_path[0] == '/' and new_path[2] == '/': | |
| new_path = new_path[0] + new_path[1].lower() + new_path[2:] | |
| return new_path | |
| def adjusted_path(tools_to_activate, system=False, user=False): | |
| """Looks at the current PATH and adds and removes entries so that the PATH reflects | |
| the set of given active tools. | |
| """ | |
| # These directories should be added to PATH | |
| path_add = get_required_path(tools_to_activate) | |
| # These already exist. | |
| if WINDOWS and not MSYS: | |
| existing_path = win_get_environment_variable('PATH', system=system, user=user, fallback=True).split(ENVPATH_SEPARATOR) | |
| else: | |
| existing_path = os.environ['PATH'].split(ENVPATH_SEPARATOR) | |
| existing_emsdk_tools = [] | |
| existing_nonemsdk_path = [] | |
| for entry in existing_path: | |
| if to_unix_path(entry).startswith(EMSDK_PATH): | |
| existing_emsdk_tools.append(entry) | |
| else: | |
| existing_nonemsdk_path.append(entry) | |
| new_emsdk_tools = [] | |
| kept_emsdk_tools = [] | |
| for entry in path_add: | |
| if not normalized_contains(existing_emsdk_tools, entry): | |
| new_emsdk_tools.append(entry) | |
| else: | |
| kept_emsdk_tools.append(entry) | |
| whole_path = unique_items(new_emsdk_tools + kept_emsdk_tools + existing_nonemsdk_path) | |
| if MSYS: | |
| # XXX Hack: If running native Windows Python in MSYS prompt where PATH | |
| # entries look like "/c/Windows/System32", os.environ['PATH'] | |
| # in Python will transform to show them as "C:\\Windows\\System32", so need | |
| # to reconvert path delimiter back to forward slashes. | |
| whole_path = [to_msys_path(p) for p in whole_path] | |
| new_emsdk_tools = [to_msys_path(p) for p in new_emsdk_tools] | |
| separator = ':' if MSYS else ENVPATH_SEPARATOR | |
| return (separator.join(whole_path), new_emsdk_tools) | |
| def get_env_vars_to_add(tools_to_activate, system, user): | |
| env_vars_to_add = [] | |
| newpath, added_path = adjusted_path(tools_to_activate, system, user) | |
| # Don't bother setting the path if there are no changes. | |
| if os.environ['PATH'] != newpath: | |
| env_vars_to_add += [('PATH', newpath)] | |
| if added_path: | |
| info('Adding directories to PATH:') | |
| for item in added_path: | |
| info('PATH += ' + item) | |
| info('') | |
| # A core variable EMSDK points to the root of Emscripten SDK directory. | |
| env_vars_to_add += [('EMSDK', EMSDK_PATH)] | |
| for tool in tools_to_activate: | |
| for env in tool.activated_environment(): | |
| key, value = parse_key_value(env) | |
| value = to_native_path(tool.expand_vars(value)) | |
| env_vars_to_add += [(key, value)] | |
| emroot = find_emscripten_root(tools_to_activate) | |
| if emroot: | |
| # For older emscripten versions that don't use an embedded cache by | |
| # default we need to export EM_CACHE. | |
| # | |
| # Sadly, we can't put this in the config file since those older versions | |
| # also didn't read the `CACHE` key from the config file: | |
| # | |
| # History: | |
| # - 'CACHE' config started being honored in 1.39.16 | |
| # https://github.com/emscripten-core/emscripten/pull/11091 | |
| # - Default to embedded cache also started in 1.39.16 | |
| # https://github.com/emscripten-core/emscripten/pull/11126 | |
| # - Emscripten supports automatically locating the embedded | |
| # config in 1.39.13: | |
| # https://github.com/emscripten-core/emscripten/pull/10935 | |
| # | |
| # Since setting EM_CACHE in the environment effects the entire machine | |
| # we want to avoid this except when installing these older emscripten | |
| # versions that really need it. | |
| version = parse_emscripten_version(emroot) | |
| if version < [1, 39, 16]: | |
| em_cache_dir = os.path.join(emroot, 'cache') | |
| env_vars_to_add += [('EM_CACHE', em_cache_dir)] | |
| if version < [1, 39, 13]: | |
| env_vars_to_add += [('EM_CONFIG', os.path.normpath(EM_CONFIG_PATH))] | |
| return env_vars_to_add | |
| def construct_env(tools_to_activate, system, user): | |
| info('Setting up EMSDK environment (suppress these messages with EMSDK_QUIET=1)') | |
| return construct_env_with_vars(get_env_vars_to_add(tools_to_activate, system, user)) | |
| def unset_env(key): | |
| if POWERSHELL: | |
| return f'Remove-Item env:{key} -ErrorAction SilentlyContinue\n' | |
| if CMD: | |
| return f'set {key}=\n' | |
| if CSH: | |
| return f'unsetenv {key};\n' | |
| if FISH: | |
| return f'set -e {key};\n' | |
| if BASH: | |
| return f'unset {key};\n' | |
| assert False | |
| def construct_env_with_vars(env_vars_to_add): | |
| env_string = '' | |
| if env_vars_to_add: | |
| info('Setting environment variables:') | |
| for key, value in env_vars_to_add: | |
| # Don't set env vars which are already set to the correct value. | |
| if key in os.environ and to_unix_path(os.environ[key]) == to_unix_path(value): | |
| continue | |
| info(f'{key} = {value}') | |
| if POWERSHELL: | |
| env_string += f'$env:{key}="{value}"\n' | |
| elif CMD: | |
| env_string += f'SET {key}={value}\n' | |
| elif CSH: | |
| env_string += f'setenv {key} "{value}";\n' | |
| elif FISH: | |
| env_string += f'set -gx {key} "{value}";\n' | |
| elif BASH: | |
| env_string += f'export {key}="{value}";\n' | |
| else: | |
| assert False | |
| if 'EMSDK_PYTHON' in env_vars_to_add: | |
| # When using our bundled python we never want the user's | |
| # PYTHONHOME or PYTHONPATH | |
| # See https://github.com/emscripten-core/emsdk/issues/598 | |
| env_string += unset_env('PYTHONHOME') | |
| env_string += unset_env('PYTHONPATH') | |
| # Remove any environment variables that might have been set by old or | |
| # inactive tools/sdks. For example, we set EM_CACHE for older versions | |
| # of the SDK but we want to remove that from the current environment | |
| # if no such tool is active. | |
| # Ignore certain keys that are inputs to emsdk itself. | |
| ignore_keys = {'EMSDK_POWERSHELL', 'EMSDK_CSH', 'EMSDK_CMD', 'EMSDK_BASH', 'EMSDK_FISH', | |
| 'EMSDK_NUM_CORES', 'EMSDK_NOTTY', 'EMSDK_KEEP_DOWNLOADS'} | |
| env_keys_to_add = {pair[0] for pair in env_vars_to_add} | |
| for key in os.environ: | |
| if key.startswith('EMSDK_') or key in {'EM_CACHE', 'EM_CONFIG'}: | |
| if key not in env_keys_to_add and key not in ignore_keys: | |
| info(f'Clearing existing environment variable: {key}') | |
| env_string += unset_env(key) | |
| return env_string | |
| def error_on_missing_tool(name): | |
| if name.endswith('-64bit') and not is_os_64bit(): | |
| exit_with_error(f"'{name}' is only provided for 64-bit OSes") | |
| else: | |
| exit_with_error(f"tool or SDK not found: '{name}'") | |
| def expand_sdk_name(name, activating): | |
| if 'upstream-master' in name: | |
| errlog('upstream-master SDK has been renamed main') | |
| name = name.replace('upstream-master', 'main') | |
| if 'fastcomp' in name: | |
| 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') | |
| if name in {'tot', 'sdk-tot', 'tot-upstream'}: | |
| if activating: | |
| # When we are activating a tot release, assume that the currently | |
| # installed SDK, if any, is the tot release we want to activate. | |
| # Without this `install tot && activate tot` will race with the builders | |
| # that are producing new builds. | |
| installed = get_installed_sdk_version() | |
| if installed: | |
| debug_print('activating currently installed SDK; not updating tot version') | |
| return f'sdk-releases-{installed}-64bit' | |
| return find_tot_sdk() | |
| if '-upstream' in name: | |
| name = name.replace('-upstream', '') | |
| name = resolve_sdk_aliases(name, verbose=True) | |
| # check if it's a release handled by an emscripten-releases version, | |
| # and if so use that by using the right hash. we support a few notations, | |
| # x.y.z | |
| # sdk-x.y.z-64bit | |
| # TODO: support short notation for old builds too? | |
| fullname = name | |
| version = fullname.replace('sdk-', '').replace('releases-', '').replace('-64bit', '').replace('tag-', '') | |
| sdk = 'sdk-' if not name.startswith('releases-') else '' | |
| releases_info = load_releases_info()['releases'] | |
| release_hash = get_release_hash(version, releases_info) | |
| if release_hash: | |
| # Known release hash | |
| full_name = f'{sdk}releases-{release_hash}-64bit' | |
| print(f"Resolving SDK version '{version}' to '{full_name}'") | |
| return full_name | |
| if len(version) == 40: | |
| global extra_release_tag | |
| extra_release_tag = version | |
| return f'{sdk}releases-{version}-64bit' | |
| return name | |
| def main(args): # ruff: ignore[complex-structure, too-many-return-statements, too-many-branches] | |
| if not args: | |
| errlog("Missing command; Type 'emsdk help' to get a list of commands.") | |
| return 1 | |
| debug_print(f'emsdk.py running under `{sys.executable}`') | |
| cmd = args.pop(0) | |
| if cmd in {'help', '--help', '-h'}: | |
| print(' emsdk: Available commands:') | |
| print(''' | |
| emsdk list [--old] [--deps] - Lists all available SDKs and tools and their | |
| current installation status. With the --old | |
| parameter, also historical versions are | |
| shown. If --deps is passed, displays the | |
| composition of different SDK packages and | |
| dependencies. | |
| emsdk update - Updates emsdk to the newest version. If you have | |
| bootstrapped emsdk via cloning directly from | |
| GitHub, call "git pull" instead to update emsdk. | |
| emsdk install [options] <tool 1> <tool 2> <tool 3> ... | |
| - Downloads and installs given tools or SDKs. | |
| Options can contain: | |
| -j<num>: Specifies the number of cores to use when | |
| building the tool. Default: use one less | |
| than the # of detected cores. | |
| --build=<type>: Controls what kind of build of LLVM to | |
| perform. Pass either 'Debug', 'Release', | |
| 'MinSizeRel' or 'RelWithDebInfo'. Default: | |
| 'Release'. | |
| --generator=<type>: Specifies the CMake Generator to be used | |
| during the build. Possible values are the | |
| same as what your CMake supports and whether | |
| the generator is valid depends on the tools | |
| you have installed. Defaults to 'Unix Makefiles' | |
| on *nix systems. If generator name is multiple | |
| words, enclose with single or double quotes. | |
| --shallow: When installing tools from one of the git | |
| development branches, this parameter can be | |
| passed to perform a shallow git clone instead | |
| of a full one. This reduces the amount of | |
| network transfer that is needed. This option | |
| should only be used when you are interested in | |
| downloading one of the development branches, | |
| but are not looking to develop Emscripten | |
| yourself. Default: disabled, i.e. do a full | |
| clone. | |
| --build-tests: If enabled, LLVM is built with internal tests | |
| included. Pass this to enable running test | |
| other.test_llvm_lit in the Emscripten test | |
| suite. Default: disabled. | |
| --enable-assertions: If specified, LLVM is built with assert() | |
| checks enabled. Useful for development | |
| purposes. Default: Enabled | |
| --disable-assertions: Forces assertions off during the build. | |
| --vs2019/--vs2022: If building from source, overrides to build | |
| using the specified compiler. When installing | |
| precompiled packages, this has no effect. | |
| Note: The same compiler specifier must be | |
| passed to the emsdk activate command to | |
| activate the desired version. | |
| Notes on building from source: | |
| To pass custom CMake directives when configuring | |
| LLVM build, specify the environment variable | |
| LLVM_CMAKE_ARGS="param1=value1,param2=value2" | |
| in the environment where the build is invoked. | |
| See README.md for details. | |
| --override-repository: Specifies the git URL to use for a given Tool. E.g. | |
| --override-repository emscripten-main@https://github.com/<fork>/emscripten/tree/<refspec> | |
| emsdk deactivate tool/sdk - Removes the given tool or SDK from the current set of activated tools. | |
| emsdk uninstall <tool/sdk> - Removes the given tool or SDK from disk.''') | |
| if WINDOWS: | |
| print(''' | |
| emsdk activate [--permanent] [--system] [--build=type] [--vs2019/--vs2022] <tool/sdk> | |
| - Activates the given tool or SDK in the | |
| environment of the current shell. | |
| - If the `--permanent` option is passed, then the environment | |
| variables are set permanently for the current user. | |
| - If the `--system` option is passed, the registration | |
| is done for all users of the system. | |
| This needs admin privileges | |
| (uses Machine environment variables). | |
| - If a custom compiler version was used to override | |
| the compiler to use, pass the same --vs2019/--vs2022 | |
| parameter here to choose which version to activate. | |
| emcmdprompt.bat - Spawns a new command prompt window with the | |
| Emscripten environment active.''') | |
| else: | |
| print(''' emsdk activate [--build=type] <tool/sdk> | |
| - Activates the given tool or SDK in the | |
| environment of the current shell.''') | |
| print(''' | |
| Both commands 'install' and 'activate' accept an optional parameter | |
| '--build=type', which can be used to override what kind of installation | |
| or activation to perform. Possible values for type are Debug, Release, | |
| MinSizeRel or RelWithDebInfo. Note: When overriding a custom build type, | |
| be sure to match the same --build= option to both 'install' and | |
| 'activate' commands and the invocation of 'emsdk_env', or otherwise | |
| these commands will default to operating on the default build type | |
| which is RelWithDebInfo.''') | |
| print(''' | |
| Environment: | |
| EMSDK_KEEP_DOWNLOADS=1 - if you want to keep the downloaded archives. | |
| EMSDK_NOTTY=1 - override isatty() result (mainly to log progress). | |
| EMSDK_NUM_CORES=n - limit parallelism to n cores. | |
| EMSDK_VERBOSE=1 - very verbose output, useful for debugging. | |
| EMSDK_RETRY_CLEAN_BUILD=1 - performs a clean rebuild of compiled tools if | |
| incremental build fails. Useful on CI.''') | |
| return 0 | |
| def extract_bool_arg(name): | |
| """Extracts a boolean command line argument from args and returns True if it was present""" | |
| if name in args: | |
| args.remove(name) | |
| return True | |
| return False | |
| def extract_string_arg(name): | |
| for i in range(len(args)): | |
| if args[i] == name: | |
| value = args[i + 1] | |
| del args[i:i + 2] | |
| return value | |
| arg_old = extract_bool_arg('--old') | |
| arg_deps = extract_bool_arg('--deps') | |
| arg_permanent = extract_bool_arg('--permanent') | |
| arg_global = extract_bool_arg('--global') | |
| arg_system = extract_bool_arg('--system') | |
| if arg_global: | |
| print('--global is deprecated. Use `--system` to set the environment variables for all users') | |
| arg_system = True | |
| if arg_system: | |
| arg_permanent = True | |
| if extract_bool_arg('--embedded'): | |
| errlog('embedded mode is now the only mode available') | |
| if extract_bool_arg('--no-embedded'): | |
| errlog('embedded mode is now the only mode available') | |
| return 1 | |
| arg_notty = extract_bool_arg('--notty') | |
| if arg_notty: | |
| global TTY_OUTPUT | |
| TTY_OUTPUT = False | |
| # Replace meta-packages with the real package names. | |
| if cmd in {'update', 'install', 'activate'}: | |
| activating = cmd == 'activate' | |
| args = [expand_sdk_name(a, activating=activating) for a in args] | |
| load_em_config() | |
| load_sdk_manifest() | |
| # Apply any overrides to git branch names to clone from. | |
| forked_url = extract_string_arg('--override-repository') | |
| while forked_url: | |
| tool_name, url_and_refspec = forked_url.split('@') | |
| t = find_tool(tool_name) | |
| if not t: | |
| errlog(f'Failed to find tool {tool_name}') | |
| return False | |
| else: | |
| t.url, t.git_branch, t.remote_name = parse_github_url_and_refspec(url_and_refspec) | |
| debug_print(f'Reading git repository URL "{t.url}" and git branch "{t.git_branch}" for Tool "{tool_name}".') | |
| forked_url = extract_string_arg('--override-repository') | |
| # Process global args | |
| for i in range(len(args)): | |
| if args[i].startswith('--generator='): | |
| build_generator = re.match(r'''^--generator=['"]?([^'"]+)['"]?$''', args[i]) | |
| if build_generator: | |
| global CMAKE_GENERATOR | |
| CMAKE_GENERATOR = build_generator.group(1) | |
| args[i] = '' | |
| else: | |
| errlog(f"Cannot parse CMake generator string: {args[i]}. Try wrapping generator string with quotes") | |
| return 1 | |
| elif args[i].startswith('--build='): | |
| build_type = re.match(r'^--build=(.+)$', args[i]) | |
| if build_type: | |
| global CMAKE_BUILD_TYPE_OVERRIDE | |
| build_type = build_type.group(1) | |
| build_types = ['Debug', 'MinSizeRel', 'RelWithDebInfo', 'Release'] | |
| try: | |
| build_type_index = [x.lower() for x in build_types].index(build_type.lower()) | |
| CMAKE_BUILD_TYPE_OVERRIDE = build_types[build_type_index] | |
| args[i] = '' | |
| except Exception: | |
| errlog(f'Unknown CMake build type "{build_type}" specified! Please specify one of {build_types}') | |
| return 1 | |
| else: | |
| errlog(f'Invalid command line parameter {args[i]} specified!') | |
| return 1 | |
| args = [x for x in args if x] | |
| if cmd == 'list': | |
| print('') | |
| def installed_sdk_text(name): | |
| sdk = find_sdk(name) | |
| return 'INSTALLED' if sdk and sdk.is_installed() else '' | |
| if (LINUX or MACOS or WINDOWS) and ARCH in {'x86', 'x86_64'}: | |
| print(f'The *recommended* precompiled SDK download is {find_latest_version()} ({find_latest_hash()}).') | |
| print() | |
| print('To install/activate it use:') | |
| print(' latest') | |
| print('') | |
| print('This is equivalent to installing/activating:') | |
| print(f' {find_latest_version()} {installed_sdk_text(find_latest_sdk())}') | |
| print('') | |
| else: | |
| print('Warning: your platform does not have precompiled SDKs available.') | |
| print('You may install components from source.') | |
| print('') | |
| print('All recent (non-legacy) installable versions are:') | |
| releases_versions = sorted(load_releases_versions(), key=version_key, reverse=True) | |
| releases_info = load_releases_info()['releases'] | |
| for ver in releases_versions: | |
| sdk_name = f'sdk-releases-{get_release_hash(ver, releases_info)}-64bit' | |
| print(f' {ver} {installed_sdk_text(sdk_name)}') | |
| print() | |
| # Use array to work around the lack of being able to mutate from enclosing | |
| # function. | |
| has_partially_active_tools = [False] | |
| if sdks: | |
| def find_sdks(needs_compilation): | |
| s = [] | |
| for sdk in sdks: | |
| if sdk.is_old and not arg_old: | |
| continue | |
| if sdk.needs_compilation() == needs_compilation: | |
| s += [sdk] | |
| return s | |
| def print_sdks(s): | |
| for sdk in s: | |
| installed = '\tINSTALLED' if sdk.is_installed() else '' | |
| active = '*' if sdk.is_active() else ' ' | |
| print(f' {active} {sdk!s: <25}{installed}') | |
| if arg_deps: | |
| for dep in sdk.deps: | |
| print(f' - {dep: <25}') | |
| print('') | |
| print('The additional following precompiled SDKs are also available for download:') | |
| print_sdks(find_sdks(False)) | |
| print('The following SDKs can be compiled from source:') | |
| print_sdks(find_sdks(True)) | |
| if tools: | |
| def find_tools(needs_compilation): | |
| t = [] | |
| for tool in tools: | |
| if tool.is_old and not arg_old: | |
| continue | |
| if tool.needs_compilation() != needs_compilation: | |
| continue | |
| t += [tool] | |
| return t | |
| def print_tools(t): | |
| for tool in t: | |
| if tool.is_old and not arg_old: | |
| continue | |
| if tool.can_be_installed() is True: | |
| installed = '\tINSTALLED' if tool.is_installed() else '' | |
| else: | |
| installed = '\tNot available: ' + tool.can_be_installed() | |
| tool_is_active = tool.is_active() | |
| tool_is_env_active = tool_is_active and tool.is_env_active() | |
| if tool_is_env_active: | |
| active = ' * ' | |
| elif tool_is_active: | |
| active = '(*)' | |
| has_partially_active_tools[0] = has_partially_active_tools[0] or True | |
| else: | |
| active = ' ' | |
| print(f' {active} {tool!s: <25}{installed}') | |
| print('') | |
| print('The following precompiled tool packages are available for download:') | |
| print_tools(find_tools(needs_compilation=False)) | |
| print('The following tools can be compiled from source:') | |
| print_tools(find_tools(needs_compilation=True)) | |
| else: | |
| if is_emsdk_sourced_from_github(): | |
| print("There are no tools available. Run 'git pull' to fetch the latest set of tools.") | |
| else: | |
| print("There are no tools available. Run 'emsdk update' to fetch the latest set of tools.") | |
| print('') | |
| print('Items marked with * are activated for the current user.') | |
| if has_partially_active_tools[0]: | |
| env_cmd = 'emsdk_env.bat' if WINDOWS else 'source ./emsdk_env.sh' | |
| print('Items marked with (*) are selected for use, ' | |
| 'but your current shell environment is not configured to use them. ' | |
| f'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 '.')) | |
| if not arg_old: | |
| print('') | |
| print("To access the historical archived versions, type 'emsdk list --old'") | |
| print('') | |
| if is_emsdk_sourced_from_github(): | |
| print('Run "git pull" to pull in the latest list.') | |
| else: | |
| print('Run "./emsdk update" to pull in the latest list.') | |
| return 0 | |
| elif cmd == 'construct_env': | |
| # Clean up old temp file up front, in case of failure later before we get | |
| # to write out the new one. | |
| tools_to_activate = currently_active_tools() | |
| tools_to_activate = process_tool_list(tools_to_activate) | |
| env_string = construct_env(tools_to_activate, arg_system, arg_permanent) | |
| if CMD or POWERSHELL: | |
| write_set_env_script(env_string) | |
| else: | |
| sys.stdout.write(env_string) | |
| return 0 | |
| elif cmd == 'update': | |
| update_emsdk() | |
| if WINDOWS: | |
| # Clean up litter after old emsdk update which may have left this temp | |
| # file around. | |
| rmfile(sdk_path(EMSDK_SET_ENV)) | |
| return 0 | |
| elif cmd == 'update-tags': | |
| errlog('`update-tags` is not longer needed. To install the latest tot release just run `install tot`') | |
| return 0 | |
| elif cmd in {'activate', 'deactivate'}: | |
| if arg_permanent: | |
| print('Registering active Emscripten environment permanently') | |
| print('') | |
| tools_to_activate = currently_active_tools() | |
| for arg in args: | |
| tool = find_tool(arg) | |
| if tool is None: | |
| tool = find_sdk(arg) | |
| if tool is None: | |
| error_on_missing_tool(arg) | |
| if cmd == 'activate': | |
| tools_to_activate += [tool] | |
| elif tool in tools_to_activate: | |
| print(f'Deactivating tool {tool}.') | |
| tools_to_activate.remove(tool) | |
| else: | |
| print(f'Tool "{arg}" was not active, no need to deactivate.') | |
| if not tools_to_activate: | |
| errlog('No tools/SDKs specified to activate! Usage:\n emsdk activate tool/sdk1 [tool/sdk2] [...]') | |
| return 1 | |
| active_tools = set_active_tools(tools_to_activate, permanently_activate=arg_permanent, system=arg_system) | |
| if not active_tools: | |
| errlog('No tools/SDKs found to activate! Usage:\n emsdk activate tool/sdk1 [tool/sdk2] [...]') | |
| return 1 | |
| if WINDOWS and not arg_permanent: | |
| 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.') | |
| return 0 | |
| elif cmd == 'install': | |
| global BUILD_FOR_TESTING, ENABLE_LLVM_ASSERTIONS, CPU_CORES, GIT_CLONE_SHALLOW | |
| # Process args | |
| for i in range(len(args)): | |
| if args[i].startswith('-j'): | |
| multicore = re.match(r'^-j(\d+)$', args[i]) | |
| if multicore: | |
| CPU_CORES = int(multicore.group(1)) | |
| args[i] = '' | |
| else: | |
| errlog(f'Invalid command line parameter {args[i]} specified!') | |
| return 1 | |
| elif args[i] == '--shallow': | |
| GIT_CLONE_SHALLOW = True | |
| args[i] = '' | |
| elif args[i] == '--build-tests': | |
| BUILD_FOR_TESTING = True | |
| args[i] = '' | |
| elif args[i] == '--enable-assertions': | |
| ENABLE_LLVM_ASSERTIONS = 'ON' | |
| args[i] = '' | |
| elif args[i] == '--disable-assertions': | |
| ENABLE_LLVM_ASSERTIONS = 'OFF' | |
| args[i] = '' | |
| args = [x for x in args if x] | |
| if not args: | |
| 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.") | |
| return 1 | |
| if LINUX and ARCH == 'arm64' and args != ['latest']: | |
| errlog('WARNING: arm64-linux binaries are not available for all releases.') | |
| errlog('See https://github.com/emscripten-core/emsdk/issues/547') | |
| for t in args: | |
| tool = find_tool(t) | |
| if tool is None: | |
| tool = find_sdk(t) | |
| if tool is None: | |
| error_on_missing_tool(t) | |
| tool.install() | |
| return 0 | |
| elif cmd == 'uninstall': | |
| if not args: | |
| errlog("Syntax error. Call 'emsdk uninstall <tool name>'. Call 'emsdk list' to obtain a list of available tools.") | |
| return 1 | |
| tool = find_tool(args[0]) | |
| if tool is None: | |
| errlog(f"Error: Tool by name '{args[0]}' was not found.") | |
| return 1 | |
| tool.uninstall() | |
| return 0 | |
| errlog(f"Unknown command '{cmd}' given! Type 'emsdk help' to get a list of commands.") | |
| return 1 | |
| if __name__ == '__main__': | |
| try: | |
| sys.exit(main(sys.argv[1:])) | |
| except KeyboardInterrupt: | |
| exit_with_error('aborted by user, exiting') | |
| sys.exit(1) |