| #!/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] |
| |
| 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_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_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/" |
| with urlopen(url) as response: |
| html = response.read().decode("utf-8") |
| |
| # 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) |
| 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, |
| } |
| 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'}: |
| # '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) |