# Copyright 2025 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 json import logging import os import re import shlex import sys from enum import Enum, auto, unique from subprocess import PIPE from tools import ( cache, colored_logger, config, diagnostics, feature_matrix, ports, shared, utils, ) from tools.settings import MEM_SIZE_SETTINGS, settings, user_settings from tools.toolchain_profiler import ToolchainProfiler from tools.utils import exit_with_error, read_file SIMD_INTEL_FEATURE_TOWER = ['-msse', '-msse2', '-msse3', '-mssse3', '-msse4.1', '-msse4.2', '-msse4', '-mavx', '-mavx2'] SIMD_NEON_FLAGS = ['-mfpu=neon'] CLANG_FLAGS_WITH_ARGS = { '-MT', '-MF', '-MJ', '-MQ', '-D', '-U', '-o', '-x', '-Xpreprocessor', '-include', '-imacros', '-idirafter', '-iprefix', '-iwithprefix', '-iwithprefixbefore', '-isysroot', '-imultilib', '-A', '-isystem', '-iquote', '-install_name', '-compatibility_version', '-mllvm', '-current_version', '-I', '-L', '-include-pch', '-u', '-undefined', '-target', '-Xlinker', '-Xclang', '-z', } # These symbol names are allowed in INCOMING_MODULE_JS_API but are not part of the # default set. EXTRA_INCOMING_JS_API = [ 'fetchSettings', 'logReadFiles', 'loadSplitModule', 'onMalloc', 'onRealloc', 'onFree', 'onSbrkGrow', ] logger = logging.getLogger('args') @unique class OFormat(Enum): # Output a relocatable object file. We use this # today for `-r` and `-shared`. OBJECT = auto() WASM = auto() JS = auto() MJS = auto() HTML = auto() BARE = auto() class EmccOptions: cpu_profiler = False dash_E = False dash_M = False dash_S = False dash_c = False dylibs: list[str] = [] embed_files: list[str] = [] emit_symbol_map = False emit_tsd = '' emrun = False exclude_files: list[str] = [] executable = False extern_post_js: list[str] = [] # after all js, external to optimized code extern_pre_js: list[str] = [] # before all js, external to optimized code fast_math = False ignore_dynamic_linking = False input_files: list[str] = [] input_language = None js_transform = None lib_dirs: list[str] = [] memory_profiler = False no_entry = False no_minify = False nodefaultlibs = False nolibc = False nostartfiles = False nostdlib = False nostdlibxx = False oformat = None # Specifies the line ending format to use for all generated text files. # Defaults to using the native EOL on each platform (\r\n on Windows, \n on # Linux & MacOS) output_eol = os.linesep output_file = None post_js: list[str] = [] # after all js post_link = False pre_js: list[str] = [] # before all js preload_files: list[str] = [] relocatable = False reproduce = None requested_debug = None sanitize: set[str] = set() sanitize_minimal_runtime = False s_args: list[str] = [] save_temps = False shared = False shell_html = None source_map_base = '' syntax_only = False target = '' use_closure_compiler = None use_preload_cache = False use_preload_plugins = False valid_abspaths: list[str] = [] # Global/singleton EmccOptions options = EmccOptions() def is_unsigned_int(s): try: return int(s) >= 0 except ValueError: return False def version_string(): # if the emscripten folder is not a git repo, don't run git show - that can # look up and find the revision in a parent directory that is a git repo revision_suffix = '' if os.path.exists(utils.path_from_root('.git')): git_rev = utils.run_process( ['git', 'rev-parse', 'HEAD'], stdout=PIPE, stderr=PIPE, cwd=utils.path_from_root()).stdout.strip() revision_suffix = ' (%s)' % git_rev elif os.path.exists(utils.path_from_root('emscripten-revision.txt')): rev = read_file(utils.path_from_root('emscripten-revision.txt')).strip() revision_suffix = ' (%s)' % rev return f'emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) {utils.EMSCRIPTEN_VERSION}{revision_suffix}' def is_valid_abspath(path_name): # Any path that is underneath the emscripten repository root must be ok. if utils.normalize_path(path_name).startswith(utils.normalize_path(utils.path_from_root())): return True def in_directory(root, child): # make both path absolute root = os.path.realpath(root) child = os.path.realpath(child) # return true, if the common prefix of both is equal to directory # e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b return os.path.commonprefix([root, child]) == root for valid_abspath in options.valid_abspaths: if in_directory(valid_abspath, path_name): return True return False def is_dash_s_for_emcc(args, i): # -s OPT=VALUE or -s OPT or -sOPT are all interpreted as emscripten flags. # -s by itself is a linker option (alias for --strip-all) if args[i] == '-s': if len(args) <= i + 1: return False arg = args[i + 1] else: arg = args[i].removeprefix('-s') arg = arg.split('=')[0] return arg.isidentifier() and arg.isupper() def parse_s_args(): for arg in options.s_args: assert arg.startswith('-s') arg = arg.removeprefix('-s') # If not = is specified default to 1 if '=' in arg: key, value = arg.split('=', 1) else: key = arg value = '1' # Special handling of browser version targets. A version -1 means that the specific version # is not supported at all. Replace those with INT32_MAX to make it possible to compare e.g. # #if MIN_FIREFOX_VERSION < 68 if re.match(r'MIN_.*_VERSION', key): try: if int(value) < 0: value = '0x7FFFFFFF' except Exception: pass key, value = normalize_boolean_setting(key, value) user_settings[key] = value def parse_args(newargs): # noqa: C901, PLR0912, PLR0915 """Future modifications should consider refactoring to reduce complexity. * The McCabe cyclomatiic complexity is currently 117 vs 10 recommended. * There are currently 115 branches vs 12 recommended. * There are currently 302 statements vs 50 recommended. To revalidate these numbers, run `ruff check --select=C901,PLR091`. """ should_exit = False skip = False builtin_settings = set(settings.keys()) LEGACY_ARGS = {'--js-opts', '--llvm-opts', '--llvm-lto', '--memory-init-file'} LEGACY_FLAGS = {'--separate-asm', '--jcache', '--proxy-to-worker', '--default-obj-ext', '--embind-emit-tsd', '--remove-duplicates', '--no-heap-copy'} for i in range(len(newargs)): if skip: skip = False continue # Support legacy '--bind' flag, by mapping to `-lembind` which now # has the same effect if newargs[i] == '--bind': newargs[i] = '-lembind' arg = newargs[i] arg_value = None if arg in CLANG_FLAGS_WITH_ARGS: # Ignore the next argument rather than trying to parse it. This is needed # because that next arg could, for example, start with `-o` and we don't want # to confuse that with a normal `-o` flag. skip = True def check_flag(value): # Check for and consume a flag if arg == value: newargs[i] = '' return True return False def check_arg(name): nonlocal arg, arg_value if arg.startswith(name) and '=' in arg: arg, arg_value = arg.split('=', 1) newargs[i] = '' return True if arg == name: if len(newargs) <= i + 1: exit_with_error(f"option '{arg}' requires an argument") arg_value = newargs[i + 1] newargs[i] = '' newargs[i + 1] = '' return True return False def consume_arg(): nonlocal arg_value assert arg_value is not None rtn = arg_value arg_value = None return rtn def consume_arg_file(): name = consume_arg() if not os.path.isfile(name): exit_with_error("'%s': file not found: '%s'" % (arg, name)) return name if arg in LEGACY_FLAGS: diagnostics.warning('deprecated', f'{arg} is no longer supported') continue for l in LEGACY_ARGS: if check_arg(l): consume_arg() diagnostics.warning('deprecated', f'{arg} is no longer supported') continue if arg.startswith('-s') and is_dash_s_for_emcc(newargs, i): s_arg = arg if arg == '-s': s_arg = '-s' + newargs[i + 1] newargs[i + 1] = '' newargs[i] = '' options.s_args.append(s_arg) elif arg.startswith('-O'): # Let -O default to -O2, which is what gcc does. opt_level = arg.removeprefix('-O') or '2' if opt_level == 's': opt_level = 2 settings.SHRINK_LEVEL = 1 elif opt_level == 'z': opt_level = 2 settings.SHRINK_LEVEL = 2 elif opt_level == 'g': opt_level = 1 settings.SHRINK_LEVEL = 0 settings.DEBUG_LEVEL = max(settings.DEBUG_LEVEL, 1) elif opt_level == 'fast': # -Ofast typically includes -ffast-math semantics options.fast_math = True opt_level = 3 settings.SHRINK_LEVEL = 0 else: settings.SHRINK_LEVEL = 0 try: level = int(opt_level) except ValueError: exit_with_error(f"invalid integral value '{opt_level}' in '{arg}'") if level > 3 or level < 0: diagnostics.warn(f"optimization level '{arg}' is not supported; using '-O3' instead") newargs[i] = '-O3' level = 3 settings.OPT_LEVEL = level elif arg.startswith('-flto'): if '=' in arg: settings.LTO = arg.split('=')[1] else: settings.LTO = 'full' elif arg == "-fno-lto": settings.LTO = 0 elif arg == "--save-temps": options.save_temps = True elif check_arg('--closure-args'): args = consume_arg() settings.CLOSURE_ARGS += shlex.split(args) elif check_arg('--closure'): options.use_closure_compiler = int(consume_arg()) elif check_arg('--js-transform'): options.js_transform = consume_arg() elif check_arg('--reproduce'): options.reproduce = consume_arg() elif check_arg('--pre-js'): options.pre_js.append(consume_arg_file()) elif check_arg('--post-js'): options.post_js.append(consume_arg_file()) elif check_arg('--extern-pre-js'): options.extern_pre_js.append(consume_arg_file()) elif check_arg('--extern-post-js'): options.extern_post_js.append(consume_arg_file()) elif check_arg('--compiler-wrapper'): config.COMPILER_WRAPPER = consume_arg() elif check_flag('--post-link'): options.post_link = True elif check_arg('--oformat'): formats = [f.lower() for f in OFormat.__members__] fmt = consume_arg() if fmt not in formats: exit_with_error('invalid output format: `%s` (must be one of %s)' % (fmt, formats)) options.oformat = getattr(OFormat, fmt.upper()) elif check_arg('--minify'): arg = consume_arg() if arg != '0': exit_with_error('0 is the only supported option for --minify; 1 has been deprecated') options.no_minify = True elif arg.startswith('-g'): options.requested_debug = arg debug_level = arg.removeprefix('-g') or '3' if is_unsigned_int(debug_level): # the -gX value is the debug level (-g1, -g2, etc.) debug_level = int(debug_level) settings.DEBUG_LEVEL = debug_level if debug_level == 0: # Set these explicitly so -g0 overrides previous -g on the cmdline settings.GENERATE_DWARF = 0 settings.GENERATE_SOURCE_MAP = 0 settings.EMIT_NAME_SECTION = 0 elif debug_level > 1: settings.EMIT_NAME_SECTION = 1 # if we don't need to preserve LLVM debug info, do not keep this flag # for clang if debug_level < 3 and not (settings.GENERATE_SOURCE_MAP or settings.SEPARATE_DWARF): newargs[i] = '-g0' else: if debug_level == 3: settings.GENERATE_DWARF = 1 elif debug_level == 4: # In the past we supported, -g4. But clang never did. # Lower this to -g3, and report a warning. newargs[i] = '-g3' diagnostics.warning('deprecated', 'please replace -g4 with -gsource-map') settings.GENERATE_SOURCE_MAP = 1 elif debug_level > 4: exit_with_error("unknown argument: '%s'", arg) else: if debug_level.startswith('force_dwarf'): exit_with_error('gforce_dwarf was a temporary option and is no longer necessary (use -g)') elif debug_level.startswith('separate-dwarf'): # emit full DWARF but also emit it in a file on the side newargs[i] = '-g' # if a file is provided, use that; otherwise use the default location # (note that we do not know the default location until all args have # been parsed, so just note True for now). if debug_level != 'separate-dwarf': if not debug_level.startswith('separate-dwarf=') or debug_level.count('=') != 1: exit_with_error('invalid -gseparate-dwarf=FILENAME notation') settings.SEPARATE_DWARF = debug_level.split('=')[1] else: settings.SEPARATE_DWARF = True settings.GENERATE_DWARF = 1 settings.DEBUG_LEVEL = 3 elif debug_level in {'source-map', 'source-map=inline'}: settings.GENERATE_SOURCE_MAP = 1 if debug_level == 'source-map' else 2 newargs[i] = '-g' elif debug_level == 'z': # Ignore `-gz`. We don't support debug info compression. pass else: # Other non-integer levels (e.g. -gline-tables-only or -gdwarf-5) are # usually clang flags that emit DWARF. So we pass them through to # clang and make the emscripten code treat it like any other DWARF. settings.GENERATE_DWARF = 1 settings.EMIT_NAME_SECTION = 1 settings.DEBUG_LEVEL = 3 elif check_flag('-profiling') or check_flag('--profiling'): settings.DEBUG_LEVEL = max(settings.DEBUG_LEVEL, 2) settings.EMIT_NAME_SECTION = 1 elif check_flag('-profiling-funcs') or check_flag('--profiling-funcs'): settings.EMIT_NAME_SECTION = 1 elif newargs[i] == '--tracing' or newargs[i] == '--memoryprofiler': if newargs[i] == '--memoryprofiler': options.memory_profiler = True newargs[i] = '' settings.EMSCRIPTEN_TRACING = 1 elif check_flag('--emit-symbol-map'): options.emit_symbol_map = True settings.EMIT_SYMBOL_MAP = 1 elif check_arg('--emit-minification-map'): settings.MINIFICATION_MAP = consume_arg() elif check_arg('--embed-file'): options.embed_files.append(consume_arg()) elif check_arg('--preload-file'): options.preload_files.append(consume_arg()) elif check_arg('--exclude-file'): options.exclude_files.append(consume_arg()) elif check_flag('--use-preload-cache'): options.use_preload_cache = True elif check_flag('--use-preload-plugins'): options.use_preload_plugins = True elif check_flag('--ignore-dynamic-linking'): options.ignore_dynamic_linking = True elif arg == '-v': shared.PRINT_SUBPROCS = True elif arg == '-###': shared.SKIP_SUBPROCS = True elif check_arg('--shell-file'): options.shell_html = consume_arg_file() elif check_arg('--source-map-base'): options.source_map_base = consume_arg() elif check_arg('--emit-tsd'): options.emit_tsd = consume_arg() elif check_flag('--no-entry'): options.no_entry = True elif check_arg('--cache'): config.CACHE = os.path.abspath(consume_arg()) cache.setup() # Ensure child processes share the same cache (e.g. when using emcc to compiler system # libraries) os.environ['EM_CACHE'] = config.CACHE elif check_flag('--clear-cache'): logger.info('clearing cache as requested by --clear-cache: `%s`', cache.cachedir) cache.erase() shared.perform_sanity_checks() # this is a good time for a sanity check should_exit = True elif check_flag('--clear-ports'): logger.info('clearing ports and cache as requested by --clear-ports') ports.clear() cache.erase() shared.perform_sanity_checks() # this is a good time for a sanity check should_exit = True elif check_flag('--check'): print(version_string(), file=sys.stderr) shared.check_sanity(force=True) should_exit = True elif check_flag('--show-ports'): ports.show_ports() should_exit = True elif check_arg('--valid-abspath'): options.valid_abspaths.append(consume_arg()) elif arg.startswith(('-I', '-L')): path_name = arg[2:] # Look for '/' explicitly so that we can also diagnose identically if -I/foo/bar is passed on Windows. # Python since 3.13 does not treat '/foo/bar' as an absolute path on Windows. if (path_name.startswith('/') or os.path.isabs(path_name)) and not is_valid_abspath(path_name): # Of course an absolute path to a non-system-specific library or header # is fine, and you can ignore this warning. The danger are system headers # that are e.g. x86 specific and non-portable. The emscripten bundled # headers are modified to be portable, local system ones are generally not. diagnostics.warning( 'absolute-paths', f'-I or -L of an absolute path "{arg}" ' 'encountered. If this is to a local system header/library, it may ' 'cause problems (local system files make sense for compiling natively ' 'on your system, but not necessarily to JavaScript).') if arg.startswith('-L'): options.lib_dirs.append(path_name) elif check_flag('--emrun'): options.emrun = True elif check_flag('--cpuprofiler'): options.cpu_profiler = True elif check_flag('--threadprofiler'): settings.PTHREADS_PROFILING = 1 elif arg in {'-fcolor-diagnostics', '-fdiagnostics-color', '-fdiagnostics-color=always'}: colored_logger.enable(force=True) elif arg in {'-fno-color-diagnostics', '-fno-diagnostics-color', '-fdiagnostics-color=never'}: colored_logger.disable() elif arg == '-fno-exceptions': settings.DISABLE_EXCEPTION_CATCHING = 1 settings.DISABLE_EXCEPTION_THROWING = 1 elif arg == '-mbulk-memory': feature_matrix.enable_feature(feature_matrix.Feature.BULK_MEMORY, '-mbulk-memory', override=True) elif arg == '-mno-bulk-memory': feature_matrix.disable_feature(feature_matrix.Feature.BULK_MEMORY) elif arg == '-msign-ext': feature_matrix.enable_feature(feature_matrix.Feature.SIGN_EXT, '-msign-ext', override=True) elif arg == '-mno-sign-ext': feature_matrix.disable_feature(feature_matrix.Feature.SIGN_EXT) elif arg == '-mnontrapping-fptoint': feature_matrix.enable_feature(feature_matrix.Feature.NON_TRAPPING_FPTOINT, '-mnontrapping-fptoint', override=True) elif arg == '-mno-nontrapping-fptoint': feature_matrix.disable_feature(feature_matrix.Feature.NON_TRAPPING_FPTOINT) elif arg == '-fexceptions': # TODO Currently -fexceptions only means Emscripten EH. Switch to wasm # exception handling by default when -fexceptions is given when wasm # exception handling becomes stable. settings.DISABLE_EXCEPTION_THROWING = 0 settings.DISABLE_EXCEPTION_CATCHING = 0 elif arg == '-fwasm-exceptions': settings.WASM_EXCEPTIONS = 1 elif arg == '-fignore-exceptions': settings.DISABLE_EXCEPTION_CATCHING = 1 elif arg == '-ffast-math': options.fast_math = True elif arg.startswith('-fsanitize=cfi'): exit_with_error('emscripten does not currently support -fsanitize=cfi') elif check_arg('--output_eol') or check_arg('--output-eol'): style = consume_arg() if style.lower() == 'windows': options.output_eol = '\r\n' elif style.lower() == 'linux': options.output_eol = '\n' else: exit_with_error(f'invalid value for --output-eol: `{style}`') # Record PTHREADS setting because it controls whether --shared-memory is passed to lld elif arg == '-pthread': settings.PTHREADS = 1 # Also set the legacy setting name, in case use JS code depends on it. settings.USE_PTHREADS = 1 elif arg == '-no-pthread': settings.PTHREADS = 0 # Also set the legacy setting name, in case use JS code depends on it. settings.USE_PTHREADS = 0 elif arg == '-pthreads': exit_with_error('unrecognized command-line option `-pthreads`; did you mean `-pthread`?') elif arg == '-fno-rtti': settings.USE_RTTI = 0 elif arg == '-frtti': settings.USE_RTTI = 1 elif arg.startswith('-jsD'): key = arg.removeprefix('-jsD') if '=' in key: key, value = key.split('=', 1) else: value = '1' if key in builtin_settings: exit_with_error(f'{arg}: cannot change built-in settings values with a -jsD directive. Pass -s{key}={value} instead!') # Allow overrides/duplicates for user-defined -jsD flags settings[key] = value newargs[i] = '' elif check_flag('-shared'): options.shared = True elif check_flag('-r'): options.relocatable = True elif arg.startswith('-o'): options.output_file = arg.removeprefix('-o') elif check_flag('-m64'): settings.MEMORY64 = 1 elif check_flag('-m32'): settings.MEMORY64 = 0 elif check_arg('-target') or check_arg('--target'): options.target = consume_arg() if options.target not in {'wasm32', 'wasm64', 'wasm64-unknown-emscripten', 'wasm32-unknown-emscripten'}: exit_with_error(f'unsupported target: {options.target} (emcc only supports wasm64-unknown-emscripten and wasm32-unknown-emscripten)') elif check_arg('--use-port'): ports.handle_use_port_arg(settings, consume_arg()) elif arg in {'-c', '--precompile'}: options.dash_c = True elif arg == '-S': options.dash_S = True elif arg == '-E': options.dash_E = True elif arg in {'-M', '-MM'}: options.dash_M = True elif arg.startswith('-x'): # TODO(sbc): Handle multiple -x flags on the same command line options.input_language = arg elif arg == '-fsyntax-only': options.syntax_only = True elif arg in SIMD_INTEL_FEATURE_TOWER or arg in SIMD_NEON_FLAGS: # SSEx is implemented on top of SIMD128 instruction set, but do not pass SSE flags to LLVM # so it won't think about generating native x86 SSE code. newargs[i] = '' elif arg == '-nostdlib': options.nostdlib = True elif arg == '-nostdlibxx': options.nostdlibxx = True elif arg == '-nodefaultlibs': options.nodefaultlibs = True elif arg == '-nolibc': options.nolibc = True elif arg == '-nostartfiles': options.nostartfiles = True elif arg == '-fsanitize-minimal-runtime': options.sanitize_minimal_runtime = True elif arg.startswith('-fsanitize='): options.sanitize.update(arg.split('=', 1)[1].split(',')) elif arg.startswith('-fno-sanitize='): options.sanitize.difference_update(arg.split('=', 1)[1].split(',')) elif arg and (arg == '-' or not arg.startswith('-')): options.input_files.append(arg) if should_exit: sys.exit(0) return [a for a in newargs if a] def expand_byte_size_suffixes(value): """Convert a string with KB/MB size suffix, such as "32MB", to number of bytes.""" value = value.strip() match = re.match(r'^(\d+)\s*([kmgt]?b)?$', value, re.I) if not match: exit_with_error("invalid byte size `%s`. Valid suffixes are: kb, mb, gb, tb" % value) value, suffix = match.groups() value = int(value) if suffix: size_suffixes = {suffix: 1024 ** i for i, suffix in enumerate(['b', 'kb', 'mb', 'gb', 'tb'])} value *= size_suffixes[suffix.lower()] return value def parse_symbol_list_file(contents): """Parse contents of one-symbol-per-line response file. This format can by used with, for example, -sEXPORTED_FUNCTIONS=@filename and avoids the need for any kind of quoting or escaping. """ values = contents.splitlines() return [v.strip() for v in values if not v.startswith('#')] def parse_value(text, expected_type): # Note that using response files can introduce whitespace, if the file # has a newline at the end. For that reason, we rstrip() in relevant # places here. def parse_string_value(text): first = text[0] if first in {"'", '"'}: text = text.rstrip() if text[-1] != text[0] or len(text) < 2: raise ValueError(f'unclosed quoted string. expected final character to be "{text[0]}" and length to be greater than 1 in "{text[0]}"') return text[1:-1] return text def parse_string_list_members(text): sep = ',' values = text.split(sep) result = [] index = 0 while True: current = values[index].lstrip() # Cannot safely rstrip for cases like: "HERE-> ," if not len(current): raise ValueError('empty value in string list') first = current[0] if first not in {"'", '"'}: result.append(current.rstrip()) else: start = index while True: # Continue until closing quote found if index >= len(values): raise ValueError(f"unclosed quoted string. expected final character to be '{first}' in '{values[start]}'") new = values[index].rstrip() if new and new[-1] == first: if start == index: result.append(current.rstrip()[1:-1]) else: result.append((current + sep + new)[1:-1]) break else: current += sep + values[index] index += 1 index += 1 if index >= len(values): break return result def parse_string_list(text): text = text.rstrip() if text and text[0] == '[': if text[-1] != ']': raise ValueError('unterminated string list. expected final character to be "]"') text = text[1:-1] if not text.strip(): return [] return parse_string_list_members(text) if expected_type == list or (text and text[0] == '['): # if json parsing fails, we fall back to our own parser, which can handle a few # simpler syntaxes try: parsed = json.loads(text) except ValueError: return parse_string_list(text) # if we succeeded in parsing as json, check some properties of it before returning if type(parsed) not in {str, list}: raise ValueError(f'settings must be strings or lists (not {type(parsed)})') if type(parsed) is list: for elem in parsed: if type(elem) is not str: raise ValueError(f'list members in settings must be strings (not {type(elem)})') return parsed if expected_type == float: try: return float(text) except ValueError: pass try: if text.startswith('0x'): base = 16 else: base = 10 return int(text, base) except ValueError: return parse_string_value(text) def apply_user_settings(): """Take a map of users settings {NAME: VALUE} and apply them to the global settings object.""" # Stash a copy of all available incoming APIs before the user can potentially override it settings.ALL_INCOMING_MODULE_JS_API = settings.INCOMING_MODULE_JS_API + EXTRA_INCOMING_JS_API for key, value in user_settings.items(): if key in settings.internal_settings: exit_with_error('%s is an internal setting and cannot be set from command line', key) # map legacy settings which have aliases to the new names # but keep the original key so errors are correctly reported via the `setattr` below user_key = key if key in settings.legacy_settings and key in settings.alt_names: key = settings.alt_names[key] # In those settings fields that represent amount of memory, translate suffixes to multiples of 1024. if key in MEM_SIZE_SETTINGS: value = str(expand_byte_size_suffixes(value)) filename = None if value and value[0] == '@': filename = value.removeprefix('@') if not os.path.isfile(filename): exit_with_error('%s: file not found parsing argument: %s=%s' % (filename, key, value)) value = read_file(filename).strip() else: value = value.replace('\\', '\\\\') expected_type = settings.types.get(key) if filename and expected_type == list and value.strip()[0] != '[': # Prefer simpler one-line-per value parser value = parse_symbol_list_file(value) else: try: value = parse_value(value, expected_type) except Exception as e: exit_with_error(f'error parsing "-s" setting "{key}={value}": {e}') setattr(settings, user_key, value) if key == 'EXPORTED_FUNCTIONS': # used for warnings in emscripten.py settings.USER_EXPORTS = settings.EXPORTED_FUNCTIONS.copy() if key == 'JSPI': settings.ASYNCIFY = 2 if key == 'JSPI_IMPORTS': settings.ASYNCIFY_IMPORTS = value if key == 'JSPI_EXPORTS': settings.ASYNCIFY_EXPORTS = value def normalize_boolean_setting(name, value): # boolean NO_X settings are aliases for X # (note that *non*-boolean setting values have special meanings, # and we can't just flip them, so leave them as-is to be # handled in a special way later) if name.startswith('NO_') and value in {'0', '1'}: name = name.removeprefix('NO_') value = str(1 - int(value)) return name, value def normalize_args(args): """Normalize argument that can be specific as either one or two arguments. In some cases these arguments are simply joined together. For example [`-o` `foo`] becomes `-ofoo` and [`-L` `bar`] becomes `-Lbar`. In other cases they are joined by an equals sign. For example ['--js-library`, `foo.js`] becomes `--js-library=foo.js`. """ equals_args = {'--js-library'} join_args = {'-l', '-L', '-I', '-z', '-o', '-x', '-u'} | equals_args for i in range(len(args)): if args[i] in join_args: if args[i] in equals_args: args[i] += '=' if len(args) <= i + 1: exit_with_error(f"option '{args[i]}' requires an argument") args[i] += args[i + 1] args[i + 1] = '' return [a for a in args if a] @ToolchainProfiler.profile() def parse_arguments(args): newargs = list(args) # Scan and strip emscripten specific cmdline warning flags. # This needs to run before other cmdline flags have been parsed, so that # warnings are properly printed during arg parse. newargs = diagnostics.capture_warnings(newargs) if not diagnostics.is_enabled('deprecated'): settings.WARN_DEPRECATED = 0 newargs = normalize_args(newargs) newargs = parse_args(newargs) if options.post_link or options.oformat == OFormat.BARE: diagnostics.warning('experimental', '--oformat=bare/--post-link are experimental and subject to change.') parse_s_args() # STRICT is used when applying settings so it needs to be applied first before # calling `apply_user_settings`. strict_cmdline = user_settings.get('STRICT') if strict_cmdline: settings.STRICT = int(strict_cmdline) # Apply -s args here (after optimization levels, so they can override them) apply_user_settings() return newargs