# Copyright 2011 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 base64
import json
import logging
import os
import re
import shlex
import shutil
import stat
from subprocess import PIPE
from urllib.parse import quote

from . import (
  building,
  cache,
  config,
  diagnostics,
  emscripten,
  extract_metadata,
  feature_matrix,
  js_manipulation,
  ports,
  shared,
  system_libs,
  utils,
  webassembly,
)
from .cmdline import OFormat
from .feature_matrix import Feature
from .minimal_runtime_shell import generate_minimal_runtime_html
from .settings import (
  DEPRECATED_SETTINGS,
  EXPERIMENTAL_SETTINGS,
  INCOMPATIBLE_SETTINGS,
  JS_ONLY_SETTINGS,
  default_setting,
  settings,
  user_settings,
)
from .shared import DEBUG, DYLIB_EXTENSIONS, do_replace, in_temp
from .toolchain_profiler import ToolchainProfiler
from .utils import (
  WINDOWS,
  delete_file,
  exit_with_error,
  get_file_suffix,
  read_file,
  safe_copy,
  unsuffixed,
  unsuffixed_basename,
  write_file,
)

logger = logging.getLogger('link')

DEFAULT_SHELL_HTML = utils.path_from_root('html/shell.html')

DEFAULT_ASYNCIFY_IMPORTS = ['__asyncjs__*']

DEFAULT_ASYNCIFY_EXPORTS = [
  'main',
  '__main_argc_argv',
]

VALID_ENVIRONMENTS = {'web', 'webview', 'worker', 'node', 'shell', 'worklet'}

EXECUTABLE_EXTENSIONS = ['.wasm', '.html', '.js', '.mjs', '.out', '']

# Supported LLD flags which we will pass through to the linker.
SUPPORTED_LINKER_FLAGS = (
    '--start-group', '--end-group',
    '-(', '-)',
    '--whole-archive', '--no-whole-archive',
    '-whole-archive', '-no-whole-archive',
    '-rpath',
)

# Unsupported LLD flags which we will ignore.
# Maps to true if the flag takes an argument.
UNSUPPORTED_LLD_FLAGS = {
    # macOS-specific linker flag that libtool (ltmain.sh) will if macOS is detected.
    '-bind_at_load': False,
    # wasm-ld doesn't support soname or other dynamic linking flags (yet).   Ignore them
    # in order to aid build systems that want to pass these flags.
    '-allow-shlib-undefined': False,
    '-rpath-link': True,
    '-version-script': True,
    '-install_name': True,
}

UBSAN_SANITIZERS = {
  'alignment',
  'bool',
  'builtin',
  'bounds',
  'enum',
  'float-cast-overflow',
  'float-divide-by-zero',
  'function',
  'implicit-unsigned-integer-truncation',
  'implicit-signed-integer-truncation',
  'implicit-integer-sign-change',
  'integer-divide-by-zero',
  'nonnull-attribute',
  'null',
  'nullability-arg',
  'nullability-assign',
  'nullability-return',
  'object-size',
  'pointer-overflow',
  'return',
  'returns-nonnull-attribute',
  'shift',
  'signed-integer-overflow',
  'unreachable',
  'unsigned-integer-overflow',
  'vla-bound',
  'vptr',
  'undefined',
  'undefined-trap',
  'implicit-integer-truncation',
  'implicit-integer-arithmetic-value-change',
  'implicit-conversion',
  'integer',
  'nullability',
}


final_js = None


# this function uses the global 'final' variable, which contains the current
# final output file. if a method alters final, and calls this method, then it
# must modify final globally (i.e. it can't receive final as a param and
# return it)
# TODO: refactor all this, a singleton that abstracts over the final output
#       and saving of intermediates
def save_intermediate(name, suffix='js'):
  if not DEBUG:
    return
  if not final_js:
    logger.debug(f'(not saving intermediate {name} because not generating JS)')
    return
  building.save_intermediate(final_js, f'{name}.{suffix}')


def save_intermediate_with_wasm(name, wasm_binary):
  if not DEBUG:
    return
  save_intermediate(name) # save the js
  building.save_intermediate(wasm_binary, name + '.wasm')


def base64_encode(filename):
  data = utils.read_binary(filename)
  b64 = base64.b64encode(data)
  return b64.decode('ascii')


def align_to_wasm_page_boundary(address):
  page_size = webassembly.WASM_PAGE_SIZE
  return ((address + (page_size - 1)) // page_size) * page_size


def will_metadce():
  # The metadce JS parsing code does not currently support the JS that gets generated
  # when assertions are enabled.
  if settings.ASSERTIONS:
    return False
  return settings.OPT_LEVEL >= 3 or settings.SHRINK_LEVEL >= 1


def setup_environment_settings():
  # The worker environment is automatically added if any of the pthread or Worker features are used.
  # Note: we need to actually modify ENVIRONMENTS variable here before the parsing,
  # because some JS code reads it back so modifying parsed info alone is not sufficient.
  maybe_web_worker = not settings.ENVIRONMENT or 'worker' in settings.ENVIRONMENT

  if settings.SHARED_MEMORY and settings.ENVIRONMENT:
    settings.ENVIRONMENT.append('worker')

  if settings.AUDIO_WORKLET:
    settings.ENVIRONMENT.append('worklet')

  # Environment setting based on user input
  if any(x for x in settings.ENVIRONMENT if x not in VALID_ENVIRONMENTS):
    exit_with_error(f'Invalid environment specified in "ENVIRONMENT": {settings.ENVIRONMENT}. Should be one of: {",".join(VALID_ENVIRONMENTS)}')

  settings.ENVIRONMENT_MAY_BE_WEB = not settings.ENVIRONMENT or 'web' in settings.ENVIRONMENT
  settings.ENVIRONMENT_MAY_BE_WEBVIEW = not settings.ENVIRONMENT or 'webview' in settings.ENVIRONMENT
  settings.ENVIRONMENT_MAY_BE_NODE = not settings.ENVIRONMENT or 'node' in settings.ENVIRONMENT
  settings.ENVIRONMENT_MAY_BE_SHELL = not settings.ENVIRONMENT or 'shell' in settings.ENVIRONMENT
  settings.ENVIRONMENT_MAY_BE_WORKER = not settings.ENVIRONMENT or 'worker' in settings.ENVIRONMENT
  settings.ENVIRONMENT_MAY_BE_AUDIO_WORKLET = not settings.ENVIRONMENT or 'worklet' in settings.ENVIRONMENT

  if not settings.ENVIRONMENT_MAY_BE_NODE:
    if 'MIN_NODE_VERSION' in user_settings and settings.MIN_NODE_VERSION != feature_matrix.UNSUPPORTED:
      diagnostics.warning('unused-command-line-argument', 'ignoring MIN_NODE_VERSION because `node` environment is not enabled')
    settings.MIN_NODE_VERSION = feature_matrix.UNSUPPORTED

  if not (settings.ENVIRONMENT_MAY_BE_WEB or maybe_web_worker or settings.ENVIRONMENT_MAY_BE_WEBVIEW):
    for browser in ('FIREFOX', 'SAFARI', 'CHROME'):
      key = f'MIN_{browser}_VERSION'
      if key in user_settings and settings[key] != feature_matrix.UNSUPPORTED:
        diagnostics.warning('unused-command-line-argument', 'ignoring %s because `web`, `worker` and `webview` environments are not enabled', key)
      settings[key] = feature_matrix.UNSUPPORTED


def generate_js_sym_info():
  """Runs the js compiler to generate a list of all symbols available in the JS
  libraries.  This must be done separately for each linker invocation since the
  list of symbols depends on what settings are used.
  TODO(sbc): Find a way to optimize this.  Potentially we could add a super-set
  mode of the js compiler that would generate a list of all possible symbols
  that could be checked in.
  """
  output = emscripten.compile_javascript(symbols_only=True)
  # When running in symbols_only mode compiler.mjs outputs symbol metadata as JSON.
  return json.loads(output)


@ToolchainProfiler.profile_block('JS symbol generation')
def get_js_sym_info():
  # Avoiding using the cache when generating struct info since
  # this step is performed while the cache is locked.
  if DEBUG or settings.BOOTSTRAPPING_STRUCT_INFO or config.FROZEN_CACHE:
    return generate_js_sym_info()

  content_hash = emscripten.generate_js_compiler_input_hash(symbols_only=True)

  def generate_json():
    library_syms = generate_js_sym_info()
    return json.dumps(library_syms, separators=(',', ':'), indent=2)

  # Limit of the overall size of the cache.
  # This code will get test coverage since a full test run of `other` or `core`
  # generates ~1000 unique symbol lists.
  file_content = emscripten.get_cached_file('symbol_lists', f'{content_hash}.json', generate_json, cache_limit=500)
  return json.loads(file_content)


def filter_link_flags(flags, using_lld):
  def is_supported(f):
    if using_lld:
      for flag, takes_arg in UNSUPPORTED_LLD_FLAGS.items():
        # lld allows various flags to have either a single -foo or double --foo
        if f.startswith((flag, '-' + flag)):
          diagnostics.warning('linkflags', 'ignoring unsupported linker flag: `%s`', f)
          # Skip the next argument if this linker flag takes an argument and that
          # argument was not specified separately (i.e. it was specified as
          # single arg containing an `=` char.)
          skip_next = takes_arg and '=' not in f
          return False, skip_next
      return True, False
    else:
      if not f.startswith('-') or f in SUPPORTED_LINKER_FLAGS:
        return True, False
      # Silently ignore -l/-L flags when not using lld.  If using lld allow
      # them to pass through the linker
      if f.startswith(('-l', '-L')):
        return False, False
      diagnostics.warning('linkflags', 'ignoring unsupported linker flag: `%s`', f)
      return False, False

  results = []
  skip_next = False
  for f in flags:
    if skip_next:
      skip_next = False
      continue
    keep, skip_next = is_supported(f)
    if keep:
      results.append(f)

  return results


def fix_windows_newlines(text):
  # Avoid duplicating \r\n to \r\r\n when writing out text.
  if WINDOWS:
    text = text.replace('\r\n', '\n')
  return text


def read_js_files(files):
  contents = []
  for f in files:
    content = read_file(f)
    if content.startswith('#preprocess\n'):
      contents.append(building.read_and_preprocess(f, expand_macros=True))
    else:
      contents.append(content)
  contents = '\n'.join(contents)
  return fix_windows_newlines(contents)


def should_run_binaryen_optimizer():
  # run the binaryen optimizer in -O2+. in -O0 we don't need it obviously, while
  # in -O1 we don't run it as the LLVM optimizer has been run, and it does the
  # great majority of the work; not running the binaryen optimizer in that case
  # keeps -O1 mostly-optimized while compiling quickly and without rewriting
  # DWARF etc.
  return settings.OPT_LEVEL >= 2


def get_binaryen_lowering_passes():
  passes = []

  # The following features are all enabled in llvm by default and therefore
  # enabled in the emscripten system libraries.  This means that we need to
  # lower them away using binaryen passes, if they are not enabled in the
  # feature matrix.
  # This can happen if the feature is explicitly disabled on the command line,
  # or when targeting an VM/engine that does not support the feature.

  # List of [<feature_name>, <lowering_flag>, <feature_flags>] triples.
  features = [
    [Feature.SIGN_EXT, '--signext-lowering', ['--enable-sign-ext']],
    [Feature.NON_TRAPPING_FPTOINT, '--llvm-nontrapping-fptoint-lowering', ['--enable-nontrapping-float-to-int']],
    [Feature.BULK_MEMORY, '--llvm-memory-copy-fill-lowering', ['--enable-bulk-memory', '--enable-bulk-memory-opt']],
  ]

  for feature, lowering_flag, feature_flags in features:
    if not feature_matrix.caniuse(feature):
      logger.debug(f'lowering {feature.name} feature due to incompatible target browser engines')
      for f in feature_flags:
        # Remove features from binaryen_features, otherwise future runs of binaryen
        # could re-introduce the feature.
        if f in building.binaryen_features:
          building.binaryen_features.remove(f)
      passes.append(lowering_flag)

  return passes


def get_binaryen_passes(options):
  passes = get_binaryen_lowering_passes()
  optimizing = should_run_binaryen_optimizer()

  # safe heap must run before post-emscripten, so post-emscripten can apply the sbrk ptr
  if settings.SAFE_HEAP:
    passes += ['--safe-heap']
  if optimizing:
    # wasm-emscripten-finalize will strip the features section for us
    # automatically, but if we did not modify the wasm then we didn't run it,
    # and in an optimized build we strip it manually here. (note that in an
    # unoptimized build we might end up with the features section, if we neither
    # optimize nor run wasm-emscripten-finalize, but a few extra bytes in the
    # binary don't matter in an unoptimized build)
    passes += ['--strip-target-features']
    passes += ['--post-emscripten']
    if settings.SIDE_MODULE:
      passes += ['--pass-arg=post-emscripten-side-module']
    passes += [building.opt_level_to_str(settings.OPT_LEVEL, settings.SHRINK_LEVEL)]
    # when optimizing, use the fact that low memory is never used (1024 is a
    # hardcoded value in the binaryen pass). we also cannot do it when the stack
    # is first, as then the stack is in the low memory that should be unused.
    if settings.GLOBAL_BASE >= 1024 and not settings.STACK_FIRST:
      passes += ['--low-memory-unused']
  if options.fast_math:
    passes += ['--fast-math']
  if settings.AUTODEBUG:
    # adding '--flatten' here may make these even more effective
    passes += ['--instrument-locals']
    passes += ['--log-execution']
    passes += ['--instrument-memory']
    if settings.LEGALIZE_JS_FFI:
      # legalize it again now, as the instrumentation may need it
      passes += ['--legalize-js-interface']
      passes += building.js_legalization_pass_flags()
  if settings.EMULATE_FUNCTION_POINTER_CASTS:
    # note that this pass must run before asyncify, as if it runs afterwards we only
    # generate the  byn$fpcast_emu  functions after asyncify runs, and so we wouldn't
    # be able to further process them.
    passes += ['--fpcast-emu']
  if settings.ASYNCIFY == 1:
    passes += ['--asyncify']
    if settings.MAIN_MODULE:
      passes += ['--pass-arg=asyncify-export-globals']
    elif settings.RELOCATABLE:
      passes += ['--pass-arg=asyncify-import-globals']
    if settings.ASSERTIONS:
      passes += ['--pass-arg=asyncify-asserts']
    if settings.ASYNCIFY_ADVISE:
      passes += ['--pass-arg=asyncify-verbose']
    if settings.ASYNCIFY_IGNORE_INDIRECT:
      passes += ['--pass-arg=asyncify-ignore-indirect']
    if settings.ASYNCIFY_PROPAGATE_ADD:
      passes += ['--pass-arg=asyncify-propagate-addlist']
    passes += ['--pass-arg=asyncify-imports@%s' % ','.join(settings.ASYNCIFY_IMPORTS)]

    # shell escaping can be confusing; try to emit useful warnings
    def check_human_readable_list(items):
      for item in items:
        if item.count('(') != item.count(')'):
          logger.warning('emcc: ASYNCIFY list contains an item without balanced parentheses ("(", ")"):')
          logger.warning('   ' + item)
          logger.warning('This may indicate improper escaping that led to splitting inside your names.')
          logger.warning('Try using a response file. e.g: -sASYNCIFY_ONLY=@funcs.txt. The format is a simple')
          logger.warning('text file, one line per function.')
          break

    if settings.ASYNCIFY_REMOVE:
      check_human_readable_list(settings.ASYNCIFY_REMOVE)
      passes += ['--pass-arg=asyncify-removelist@%s' % ','.join(settings.ASYNCIFY_REMOVE)]
    if settings.ASYNCIFY_ADD:
      check_human_readable_list(settings.ASYNCIFY_ADD)
      passes += ['--pass-arg=asyncify-addlist@%s' % ','.join(settings.ASYNCIFY_ADD)]
    if settings.ASYNCIFY_ONLY:
      check_human_readable_list(settings.ASYNCIFY_ONLY)
      passes += ['--pass-arg=asyncify-onlylist@%s' % ','.join(settings.ASYNCIFY_ONLY)]

  if settings.MEMORY64 == 2:
    passes += ['--memory64-lowering', '--table64-lowering']

  if settings.BINARYEN_IGNORE_IMPLICIT_TRAPS:
    passes += ['--ignore-implicit-traps']
  # normally we can assume the memory, if imported, has not been modified
  # beforehand (in fact, in most cases the memory is not even imported anyhow,
  # but it is still safe to pass the flag), and is therefore filled with zeros.
  # the one exception is dynamic linking of a side module: the main module is ok
  # as it is loaded first, but the side module may be assigned memory that was
  # previously used.
  if optimizing and not settings.SIDE_MODULE:
    passes += ['--zero-filled-memory']
  # LLVM output always has immutable initial table contents: the table is
  # fixed and may only be appended to at runtime (that is true even in
  # relocatable mode)
  if optimizing:
    passes += ['--pass-arg=directize-initial-contents-immutable']

  if settings.BINARYEN_EXTRA_PASSES:
    # BINARYEN_EXTRA_PASSES is comma-separated, and we support both '-'-prefixed and
    # unprefixed pass names
    extras = settings.BINARYEN_EXTRA_PASSES.split(',')
    passes += [('--' + p) if p[0] != '-' else p for p in extras if p]

  # If we are going to run metadce then that means we will be running binaryen
  # tools after the main invocation, whose flags are determined here
  # (specifically we will run metadce and possibly also wasm-opt for import/
  # export minification). And when we run such a tool it will "undo" any
  # StackIR optimizations (since the conversion to BinaryenIR undoes them as it
  # restructures the code). We could re-run those opts, but it is most efficient
  # to just not do them now if we'll invoke other tools later, and we'll do them
  # only in the very last invocation.
  if will_metadce():
    passes += ['--no-stack-ir']

  return passes


def make_js_executable(script):
  src = read_file(script)

  # By default, the resulting script will run under the version of node in the PATH.
  if settings.EXECUTABLE == 1:
    settings.EXECUTABLE = '/usr/bin/env node'

  logger.debug(f'adding `#!` to JavaScript file: {settings.EXECUTABLE}')
  # add shebang
  with open(script, 'w') as f:
    f.write(f'#!{settings.EXECUTABLE}\n')
    f.write(src)
  try:
    os.chmod(script, stat.S_IMODE(os.stat(script).st_mode) | stat.S_IXUSR) # make executable
  except OSError:
    pass # can fail if e.g. writing the executable to /dev/null


def do_split_module(wasm_file, options):
  os.replace(wasm_file, wasm_file + '.orig')
  args = ['--instrument']
  if options.requested_debug:
    # Tell wasm-split to preserve function names.
    args += ['-g']
  building.run_binaryen_command('wasm-split', wasm_file + '.orig', outfile=wasm_file, args=args)


def get_worker_js_suffix():
  return '.worker.mjs' if settings.EXPORT_ES6 else '.worker.js'


def setup_pthreads():
  # pthreads + dynamic linking has certain limitations
  if settings.MAIN_MODULE or settings.RELOCATABLE:
    diagnostics.warning('experimental', 'dynamic linking + pthreads is experimental')
  if settings.ALLOW_MEMORY_GROWTH and not settings.GROWABLE_ARRAYBUFFERS:
    diagnostics.warning('pthreads-mem-growth', '-pthread + ALLOW_MEMORY_GROWTH may run non-wasm code slowly, see https://github.com/WebAssembly/design/issues/1271')

  default_setting('DEFAULT_PTHREAD_STACK_SIZE', settings.STACK_SIZE)

  # Functions needs by runtime_pthread.js
  settings.REQUIRED_EXPORTS += [
    '_emscripten_thread_free_data',
    '_emscripten_thread_crashed',
  ]

  if settings.MAIN_MODULE:
    settings.REQUIRED_EXPORTS += [
      '_emscripten_dlsync_self',
      '_emscripten_dlsync_self_async',
      '_emscripten_proxy_dlsync',
      '_emscripten_proxy_dlsync_async',
      '__dl_seterr',
    ]

  # runtime_pthread.js depends on these library symbols
  settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += [
    '$PThread',
    '$establishStackSpace',
    '$invokeEntryPoint',
  ]

  if settings.MINIMAL_RUNTIME:
    building.user_requested_exports.add('exit')


def set_initial_memory():
  user_specified_initial_heap = 'INITIAL_HEAP' in user_settings

  # INITIAL_HEAP cannot be used when the memory object is created in JS: we don't know
  # the size of static data here and thus the total initial memory size.
  if settings.IMPORTED_MEMORY:
    if user_specified_initial_heap:
      # Some of these could (and should) be implemented.
      exit_with_error('INITIAL_HEAP is currently not compatible with IMPORTED_MEMORY (which is enabled indirectly via SHARED_MEMORY, RELOCATABLE)')
    # The default for imported memory is to fall back to INITIAL_MEMORY.
    settings.INITIAL_HEAP = -1

  if not user_specified_initial_heap:
    # For backwards compatibility, we will only use INITIAL_HEAP by default when the user
    # specified neither INITIAL_MEMORY nor MAXIMUM_MEMORY. Both place an upper bounds on
    # the overall initial linear memory (stack + static data + heap), and we do not know
    # the size of static data at this stage. Setting any non-zero initial heap value in
    # this scenario would risk pushing users over the limit they have set.
    user_specified_initial = settings.INITIAL_MEMORY != -1
    user_specified_maximum = 'MAXIMUM_MEMORY' in user_settings or 'WASM_MEM_MAX' in user_settings or 'BINARYEN_MEM_MAX' in user_settings
    if user_specified_initial or user_specified_maximum:
      settings.INITIAL_HEAP = -1

  # Apply the default if we are going with INITIAL_MEMORY.
  if settings.INITIAL_HEAP == -1 and settings.INITIAL_MEMORY == -1:
    default_setting('INITIAL_MEMORY', 16 * 1024 * 1024)

  def check_memory_setting(setting):
    if settings[setting] % webassembly.WASM_PAGE_SIZE != 0:
      exit_with_error(f'{setting} must be a multiple of WebAssembly page size (64KiB), was {settings[setting]}')
    if settings[setting] >= 2**53:
      exit_with_error(f'{setting} must be smaller than 2^53 bytes due to JS Numbers (doubles) being used to hold pointer addresses in JS side')

  # Due to the aforementioned lack of knowledge about the static data size, we delegate
  # checking the overall consistency of these settings to wasm-ld.
  if settings.INITIAL_HEAP != -1:
    check_memory_setting('INITIAL_HEAP')

  if settings.INITIAL_MEMORY != -1:
    check_memory_setting('INITIAL_MEMORY')
    if settings.INITIAL_MEMORY < settings.STACK_SIZE:
      exit_with_error(f'INITIAL_MEMORY must be larger than STACK_SIZE, was {settings.INITIAL_MEMORY} (STACK_SIZE={settings.STACK_SIZE})')

  check_memory_setting('MAXIMUM_MEMORY')
  if settings.MEMORY_GROWTH_LINEAR_STEP != -1:
    check_memory_setting('MEMORY_GROWTH_LINEAR_STEP')


# Set an upper estimate of what MAXIMUM_MEMORY should be. Take note that this value
# may not be precise, and is only an upper bound of the exact value calculated later
# by the linker.
def set_max_memory():
  # With INITIAL_HEAP, we only know the lower bound on initial memory size.
  initial_memory_known = settings.INITIAL_MEMORY != -1

  if not settings.ALLOW_MEMORY_GROWTH:
    if 'MAXIMUM_MEMORY' in user_settings:
      diagnostics.warning('unused-command-line-argument', 'MAXIMUM_MEMORY is only meaningful with ALLOW_MEMORY_GROWTH')
    # Optimization: lower the default maximum memory to initial memory if possible.
    if initial_memory_known:
      settings.MAXIMUM_MEMORY = settings.INITIAL_MEMORY

  # Automatically up the default maximum when the user requested a large minimum.
  if 'MAXIMUM_MEMORY' not in user_settings:
    if settings.ALLOW_MEMORY_GROWTH:
      if any([settings.INITIAL_HEAP != -1 and settings.INITIAL_HEAP >= 2 * 1024 * 1024 * 1024,
              initial_memory_known and settings.INITIAL_MEMORY > 2 * 1024 * 1024 * 1024]):
        settings.MAXIMUM_MEMORY = 4 * 1024 * 1024 * 1024

    # INITIAL_MEMORY sets a lower bound for MAXIMUM_MEMORY
    if initial_memory_known and settings.INITIAL_MEMORY > settings.MAXIMUM_MEMORY:
      settings.MAXIMUM_MEMORY = settings.INITIAL_MEMORY

  # A similar check for INITIAL_HEAP would not be precise and so is delegated to wasm-ld.
  if initial_memory_known and settings.MAXIMUM_MEMORY < settings.INITIAL_MEMORY:
    exit_with_error('MAXIMUM_MEMORY cannot be less than INITIAL_MEMORY')


def inc_initial_memory(delta):
  # Both INITIAL_HEAP and INITIAL_MEMORY can be set at the same time. Increment both.
  if settings.INITIAL_HEAP != -1:
    settings.INITIAL_HEAP += delta
  if settings.INITIAL_MEMORY != -1:
    settings.INITIAL_MEMORY += delta


def check_browser_versions():
  # Map of setting all VM version settings to the minimum version
  # we support.
  min_version_settings = {
    'MIN_FIREFOX_VERSION': feature_matrix.OLDEST_SUPPORTED_FIREFOX,
    'MIN_CHROME_VERSION': feature_matrix.OLDEST_SUPPORTED_CHROME,
    'MIN_SAFARI_VERSION': feature_matrix.OLDEST_SUPPORTED_SAFARI,
    'MIN_NODE_VERSION': feature_matrix.OLDEST_SUPPORTED_NODE,
  }

  if settings.LEGACY_VM_SUPPORT:
    # Default all browser versions to zero
    for key in min_version_settings:
      default_setting(key, 0)

  for key, oldest in min_version_settings.items():
    if settings[key] != 0 and settings[key] < oldest:
      exit_with_error(f'{key} older than {oldest} is not supported')


def add_system_js_lib(lib):
  lib = utils.path_from_root('src/lib', lib)
  assert os.path.exists(lib)
  settings.JS_LIBRARIES.append(lib)


def check_settings():
  for s, reason in DEPRECATED_SETTINGS.items():
    if s in user_settings:
      diagnostics.warning('deprecated', f'{s} is deprecated ({reason}). Please open a bug if you have a continuing need for this setting')

  for name, msg in EXPERIMENTAL_SETTINGS.items():
    if getattr(settings, name):
      diagnostics.warning('experimental', msg)

  for a, b, reason in INCOMPATIBLE_SETTINGS:
    invert_b = b.startswith('NO_')
    if invert_b:
      b = b[3:]

    b_val = getattr(settings, b)
    if invert_b:
      b_val = not b_val

    if getattr(settings, a) and b_val:
      msg = f'{a} is not compatible with {b}'
      if invert_b:
        msg += '=0'
      if reason:
        msg += f' ({reason})'
      exit_with_error(msg)


@ToolchainProfiler.profile()
def setup_sanitizers(options):
  assert(options.sanitize)

  if settings.WASM_WORKERS:
    exit_with_error('WASM_WORKERS is not currently compatible with `-fsanitize` tools')

  if 'leak' in options.sanitize or 'address' in options.sanitize:
    # These symbols are needed by `noLeakCheck` which used to implement
    # the `__noleakcheck` attribute.  However this dependency is not yet represented in the JS
    # symbol graph generated when we run the compiler with `--symbols-only`.
    settings.REQUIRED_EXPORTS += ['__lsan_disable', '__lsan_enable']

  if ('leak' in options.sanitize or 'address' in options.sanitize) and not settings.ALLOW_MEMORY_GROWTH:
    # Increase the minimum memory requirements to account for extra memory
    # that the sanitizers might need (in addition to the shadow memory
    # requirements handled below).
    # These values are designed be an over-estimate of the actual requirements and
    # are based on experimentation with different tests/programs under asan and
    # lsan.
    inc_initial_memory(50 * 1024 * 1024)
    if settings.PTHREADS:
      inc_initial_memory(50 * 1024 * 1024)

  if options.sanitize & UBSAN_SANITIZERS:
    if options.sanitize_minimal_runtime:
      settings.UBSAN_RUNTIME = 1
    else:
      settings.UBSAN_RUNTIME = 2

  if 'leak' in options.sanitize:
    settings.USE_LSAN = 1
    default_setting('EXIT_RUNTIME', 1)

  if 'address' in options.sanitize:
    settings.USE_ASAN = 1
    default_setting('EXIT_RUNTIME', 1)
    if not settings.UBSAN_RUNTIME:
      settings.UBSAN_RUNTIME = 2

    settings.REQUIRED_EXPORTS += emscripten.ASAN_C_HELPERS

    if settings.ASYNCIFY and not settings.ASYNCIFY_ONLY:
      # we do not want asyncify to instrument these helpers - they just access
      # memory as small getters/setters, so they cannot pause anyhow, and also
      # we access them in the runtime as we prepare to rewind, which would hit
      # an asyncify assertion, if asyncify instrumented them.
      #
      # note that if ASYNCIFY_ONLY was set by the user then we do not need to
      # do anything (as the user's list won't contain these functions), and if
      # we did add them, the pass would assert on incompatible lists, hence the
      # condition in the above if.
      settings.ASYNCIFY_REMOVE.append("__asan_*")

    if settings.ASAN_SHADOW_SIZE != -1:
      diagnostics.warning('emcc', 'ASAN_SHADOW_SIZE is ignored and will be removed in a future release')

    if 'GLOBAL_BASE' in user_settings:
      exit_with_error("ASan does not support custom GLOBAL_BASE")

    # Increase the INITIAL_MEMORY and shift GLOBAL_BASE to account for
    # the ASan shadow region which starts at address zero.
    # The shadow region is 1/8th the size of the total memory and is
    # itself part of the total memory.
    # We use the following variables in this calculation:
    # - user_mem : memory usable/visible by the user program.
    # - shadow_size : memory used by asan for shadow memory.
    # - total_mem : the sum of the above. this is the size of the wasm memory (and must be aligned to WASM_PAGE_SIZE)
    user_mem = settings.MAXIMUM_MEMORY
    if not settings.ALLOW_MEMORY_GROWTH and settings.INITIAL_MEMORY != -1:
      user_mem = settings.INITIAL_MEMORY

    # Given the know value of user memory size we can work backwards
    # to find the total memory and the shadow size based on the fact
    # that the user memory is 7/8ths of the total memory.
    # (i.e. user_mem == total_mem * 7 / 8
    # TODO-Bug?: this does not look to handle 4GB MAXIMUM_MEMORY correctly.
    total_mem = user_mem * 8 / 7

    # But we might need to re-align to wasm page size
    total_mem = int(align_to_wasm_page_boundary(total_mem))

    # The shadow size is 1/8th the resulting rounded up size
    shadow_size = total_mem // 8

    # We start our global data after the shadow memory.
    # We don't need to worry about alignment here.  wasm-ld will take care of that.
    settings.GLOBAL_BASE = shadow_size

    # Adjust INITIAL_MEMORY (if needed) to account for the shifted global base.
    if settings.INITIAL_MEMORY != -1:
      if settings.ALLOW_MEMORY_GROWTH:
        settings.INITIAL_MEMORY += align_to_wasm_page_boundary(shadow_size)
      else:
        settings.INITIAL_MEMORY = total_mem

    if settings.SAFE_HEAP:
      # SAFE_HEAP instruments ASan's shadow memory accesses.
      # Since the shadow memory starts at 0, the act of accessing the shadow memory is detected
      # by SAFE_HEAP as a null pointer dereference.
      exit_with_error('ASan does not work with SAFE_HEAP')

    if settings.MEMORY64:
      exit_with_error('MEMORY64 does not yet work with ASAN')

  if settings.GENERATE_SOURCE_MAP:
    settings.LOAD_SOURCE_MAP = 1


def get_dylibs(options, linker_args):
  """Find all the Wasm dynamic libraries specified on the command line,
  either via `-lfoo` or via `libfoo.so` directly."""

  dylibs = []
  for arg in linker_args:
    if arg.startswith('-l'):
      for ext in DYLIB_EXTENSIONS:
        path = find_library('lib' + arg[2:] + ext, options.lib_dirs)
        if path and building.is_wasm_dylib(path):
          dylibs.append(path)
    elif building.is_wasm_dylib(arg):
      dylibs.append(arg)
  return dylibs


@ToolchainProfiler.profile_block('linker_setup')
def phase_linker_setup(options, linker_args):  # noqa: C901, PLR0912, PLR0915
  """Future modifications should consider refactoring to reduce complexity.

  * The McCabe cyclomatiic complexity is currently 244 vs 10 recommended.
  * There are currently 252 branches vs 12 recommended.
  * There are currently 563 statements vs 50 recommended.

  To revalidate these numbers, run `ruff check --select=C901,PLR091`.
  """

  setup_environment_settings()

  apply_library_settings(linker_args)

  if settings.SIDE_MODULE or settings.MAIN_MODULE:
    default_setting('FAKE_DYLIBS', 0)

  if options.shared and not settings.FAKE_DYLIBS:
    default_setting('SIDE_MODULE', 1)
    default_setting('RELOCATABLE', 1)

  if not settings.FAKE_DYLIBS:
    options.dylibs = get_dylibs(options, linker_args)
    # If there are any dynamically linked libraries on the command line then
    # need to enable `MAIN_MODULE` in order to produce JS code that can load them.
    if not settings.MAIN_MODULE and not settings.SIDE_MODULE and options.dylibs:
      default_setting('MAIN_MODULE', 2)

  linker_args += calc_extra_ldflags(options)

  # We used to do this check during on startup during `check_sanity`, but
  # we now only do it when linking, in order to reduce the overhead when
  # only compiling.
  if not shared.SKIP_SUBPROCS:
    shared.check_llvm_version()

  autoconf = os.environ.get('EMMAKEN_JUST_CONFIGURE') or 'conftest.c' in options.input_files or 'conftest.cpp' in options.input_files
  if autoconf:
    # configure tests want a more shell-like style, where we emit return codes on exit()
    settings.EXIT_RUNTIME = 1
    # use node.js raw filesystem access, to behave just like a native executable
    settings.NODERAWFS = 1
    # Add `#!` line to output JS and make it executable.
    settings.EXECUTABLE = config.NODE_JS[0]
    # autoconf declares functions without their proper signatures, and STRICT causes that to trip up by passing --fatal-warnings to the linker.
    if settings.STRICT:
      exit_with_error('autoconfiguring is not compatible with STRICT')

  if settings.OPT_LEVEL >= 1:
    default_setting('ASSERTIONS', 0)

  if options.emrun:
    options.pre_js.append(utils.path_from_root('src/emrun_prejs.js'))
    options.post_js.append(utils.path_from_root('src/emrun_postjs.js'))
    if settings.MINIMAL_RUNTIME:
      exit_with_error('--emrun is not compatible with MINIMAL_RUNTIME')
    # emrun mode waits on program exit
    if user_settings.get('EXIT_RUNTIME') == '0':
      exit_with_error('--emrun is not compatible with EXIT_RUNTIME=0')
    settings.EXIT_RUNTIME = 1
    # emrun_postjs.js needs this library function.
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$addOnExit']

  if options.cpu_profiler:
    options.post_js.append(utils.path_from_root('src/cpuprofiler.js'))

  # Unless RUNTIME_DEBUG is explicitly set then we enable it when any of the
  # more specific debug settings are present.
  default_setting('RUNTIME_DEBUG', int(settings.LIBRARY_DEBUG or
                                       settings.GL_DEBUG or
                                       settings.DYLINK_DEBUG or
                                       settings.OPENAL_DEBUG or
                                       settings.SYSCALL_DEBUG or
                                       settings.WEBSOCKET_DEBUG or
                                       settings.SOCKET_DEBUG or
                                       settings.FETCH_DEBUG or
                                       settings.EXCEPTION_DEBUG or
                                       settings.PTHREADS_DEBUG or
                                       settings.ASYNCIFY_DEBUG))

  if options.memory_profiler:
    settings.MEMORYPROFILER = 1

  if settings.PTHREADS_PROFILING:
    options.post_js.append(utils.path_from_root('src/threadprofiler.js'))
    settings.REQUIRED_EXPORTS.append('emscripten_main_runtime_thread_id')
    # threadprofiler.js needs these library functions.
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$addOnInit', '$addOnExit']

  # TODO: support source maps with js_transform
  if options.js_transform and settings.GENERATE_SOURCE_MAP:
    logger.warning('disabling source maps because a js transform is being done')
    settings.GENERATE_SOURCE_MAP = 0

  # options.output_file is the user-specified one, target is what we will generate
  if options.output_file:
    target = options.output_file
    # check for the existence of the output directory now, to avoid having
    # to do so repeatedly when each of the various output files (.mem, .wasm,
    # etc) are written. This gives a more useful error message than the
    # IOError and python backtrace that users would otherwise see.
    dirname = os.path.dirname(target)
    if dirname and not os.path.isdir(dirname):
      exit_with_error("specified output file (%s) is in a directory that does not exist" % target)
  elif autoconf:
    # Autoconf expects the executable output file to be called `a.out`
    target = 'a.out'
  elif settings.SIDE_MODULE:
    target = 'a.out.wasm'
  else:
    target = 'a.out.js'

  final_suffix = get_file_suffix(target)

  # Set the EXPORT_ES6 default early since it affects the setting of the
  # default oformat below.
  if settings.WASM_ESM_INTEGRATION or settings.SOURCE_PHASE_IMPORTS or settings.MODULARIZE == 'instance':
    default_setting('EXPORT_ES6', 1)

  # If no output format was specified we try to deduce the format based on
  # the output filename extension
  if not options.oformat and (options.relocatable or (options.shared and settings.FAKE_DYLIBS and not settings.SIDE_MODULE)):
    # With FAKE_DYLIBS we generate an normal object file rather than an shared object.
    # This is linked with `wasm-ld --relocatable` or (`llvm-link` in the case of LTO).
    if final_suffix in EXECUTABLE_EXTENSIONS:
      diagnostics.warning('emcc', '-shared/-r used with executable output suffix. This behaviour is deprecated.  Please remove -shared/-r to build an executable or avoid the executable suffix (%s) when building object files.' % final_suffix)
    else:
      if options.shared and 'FAKE_DYLIBS' not in user_settings:
        diagnostics.warning('emcc', 'linking a library with `-shared` will emit a static object file (FAKE_DYLIBS defaults to true).  If you want to build a runtime shared library use the SIDE_MODULE or FAKE_DYLIBS=0.')
      options.oformat = OFormat.OBJECT

  if not options.oformat:
    if settings.SIDE_MODULE or final_suffix == '.wasm':
      options.oformat = OFormat.WASM
    elif final_suffix == '.html':
      options.oformat = OFormat.HTML
    elif final_suffix == '.mjs' or settings.EXPORT_ES6:
      options.oformat = OFormat.MJS
    else:
      options.oformat = OFormat.JS

  if options.oformat in (OFormat.WASM, OFormat.OBJECT):
    for s in JS_ONLY_SETTINGS:
      if s in user_settings:
        diagnostics.warning('unused-command-line-argument', f'{s} is only valid when generating JavaScript output')

  # When there is no final suffix or the suffix is `.out` (as in `a.out`) then default to
  # making the resulting file exectuable.
  if settings.ENVIRONMENT_MAY_BE_NODE and options.oformat == OFormat.JS and final_suffix in ('', '.out'):
    default_setting('EXECUTABLE', 1)

  if settings.EXECUTABLE and not settings.ENVIRONMENT_MAY_BE_NODE:
    exit_with_error('EXECUTABLE requires `node` in ENVRIONMENT')

  if options.oformat == OFormat.MJS:
    default_setting('EXPORT_ES6', 1)

  settings.OUTPUT_FORMAT = options.oformat.name

  if settings.SUPPORT_BIG_ENDIAN and settings.WASM2JS:
    exit_with_error('WASM2JS is currently not compatible with SUPPORT_BIG_ENDIAN')

  if settings.WASM_ESM_INTEGRATION:
    default_setting('MODULARIZE', 'instance')
    if not settings.EXPORT_ES6:
      exit_with_error('WASM_ESM_INTEGRATION requires EXPORT_ES6')
    if settings.MODULARIZE != 'instance':
      exit_with_error('WASM_ESM_INTEGRATION requires MODULARIZE=instance')
    if settings.RELOCATABLE:
      exit_with_error('WASM_ESM_INTEGRATION is not compatible with dynamic linking')
    if settings.ASYNCIFY:
      exit_with_error('WASM_ESM_INTEGRATION is not compatible with -sASYNCIFY')
    if settings.WASM_WORKERS:
      exit_with_error('WASM_ESM_INTEGRATION is not compatible with WASM_WORKERS')
    if not settings.WASM_ASYNC_COMPILATION:
      exit_with_error('WASM_ESM_INTEGRATION is not compatible with WASM_ASYNC_COMPILATION=0')
    if not settings.WASM:
      exit_with_error('WASM_ESM_INTEGRATION is not compatible with WASM2JS')
    if settings.ABORT_ON_WASM_EXCEPTIONS:
      exit_with_error('WASM_ESM_INTEGRATION is not compatible with ABORT_ON_WASM_EXCEPTIONS')

  if settings.MODULARIZE and settings.MODULARIZE not in [1, 'instance']:
    exit_with_error(f'Invalid setting "{settings.MODULARIZE}" for MODULARIZE.')

  def limit_incoming_module_api():
    if options.oformat == OFormat.HTML and options.shell_html == DEFAULT_SHELL_HTML:
      # Our default shell.html file has minimal set of INCOMING_MODULE_JS_API elements that it expects
      default_setting('INCOMING_MODULE_JS_API', 'canvas,monitorRunDependencies,onAbort,onExit,print,setStatus'.split(','))
    else:
      default_setting('INCOMING_MODULE_JS_API', [])

  if settings.ASYNCIFY == 1:
    # ASYNCIFY=1 wraps only wasm exports so we need to enable legacy
    # dyncalls via dynCall_xxx exports.
    # See: https://github.com/emscripten-core/emscripten/issues/12066
    settings.DYNCALLS = 1

  if settings.MODULARIZE == 'instance':
    diagnostics.warning('experimental', 'MODULARIZE=instance is still experimental. Many features may not work or will change.')
    if not settings.EXPORT_ES6:
      exit_with_error('MODULARIZE=instance requires EXPORT_ES6')
    if settings.MINIMAL_RUNTIME:
      exit_with_error('MODULARIZE=instance is not compatible with MINIMAL_RUNTIME')
    if options.use_preload_plugins or len(options.preload_files):
      exit_with_error('MODULARIZE=instance is not compatible with --embed-file/--preload-file')

  if settings.MINIMAL_RUNTIME and len(options.preload_files):
    exit_with_error('MINIMAL_RUNTIME is not compatible with --preload-file')

  if options.oformat in (OFormat.WASM, OFormat.BARE):
    if options.emit_tsd:
      exit_with_error('Wasm only output is not compatible with --emit-tsd')
    # If the user asks directly for a wasm file then this *is* the target
    wasm_target = target
  elif settings.SINGLE_FILE or settings.WASM == 0:
    # In SINGLE_FILE or WASM2JS mode the wasm file is not part of the output at
    # all so we generate it the temp directory.
    wasm_target = in_temp(utils.replace_suffix(target, '.wasm'))
  else:
    # Otherwise the wasm file is produced alongside the final target.
    wasm_target = get_secondary_target(target, '.wasm')

  if settings.SAFE_HEAP not in [0, 1, 2]:
    exit_with_error('SAFE_HEAP must be 0, 1 or 2')

  if not settings.WASM:
    # When the user requests non-wasm output, we enable wasm2js. that is,
    # we still compile to wasm normally, but we compile the final output
    # to js.
    settings.WASM = 1
    settings.WASM2JS = 1

  if settings.WASM == 2:
    # Requesting both Wasm and Wasm2JS support
    settings.WASM2JS = 1

  if settings.WASM2JS:
    # Wasm bigint doesn't make sense with wasm2js, since it controls how the
    # wasm and JS interact.
    if user_settings.get('WASM_BIGINT') and settings.WASM_BIGINT:
      exit_with_error('WASM_BIGINT=1 is not compatible with wasm2js')
    settings.WASM_BIGINT = 0
    feature_matrix.disable_feature(Feature.JS_BIGINT_INTEGRATION)

  if options.oformat == OFormat.WASM and not settings.SIDE_MODULE:
    # if the output is just a wasm file, it will normally be a standalone one,
    # as there is no JS. an exception are side modules, as we can't tell at
    # compile time whether JS will be involved or not - the main module may
    # have JS, and the side module is expected to link against that.
    # we also do not support standalone mode in fastcomp.
    settings.STANDALONE_WASM = 1

  if settings.LZ4:
    settings.EXPORTED_RUNTIME_METHODS += ['LZ4']

  if settings.PURE_WASI:
    settings.STANDALONE_WASM = 1
    settings.WASM_BIGINT = 1
    # WASI does not support Emscripten (JS-based) exception catching, which the
    # JS-based longjmp support also uses. Emscripten EH is by default disabled
    # so we don't need to do anything here.
    if not settings.WASM_EXCEPTIONS:
      default_setting('SUPPORT_LONGJMP', 0)

  if options.no_entry:
    settings.EXPECT_MAIN = 0
  elif settings.STANDALONE_WASM:
    if '_main' in settings.EXPORTED_FUNCTIONS:
      # TODO(sbc): Make this into a warning?
      logger.debug('including `_main` in EXPORTED_FUNCTIONS is not necessary in standalone mode')
  else:
    # In normal non-standalone mode we have special handling of `_main` in EXPORTED_FUNCTIONS.
    # 1. If the user specifies exports, but doesn't include `_main` we assume they want to build a
    #    reactor.
    # 2. If the user doesn't export anything we default to exporting `_main` (unless `--no-entry`
    #    is specified (see above).
    if 'EXPORTED_FUNCTIONS' in user_settings:
      if '_main' in settings.USER_EXPORTS:
        settings.EXPORTED_FUNCTIONS.remove('_main')
        settings.EXPORT_IF_DEFINED.append('main')
      else:
        settings.EXPECT_MAIN = 0
    else:
      settings.EXPORT_IF_DEFINED.append('main')

  if settings.STANDALONE_WASM:
    # In STANDALONE_WASM mode we either build a command or a reactor.
    # See https://github.com/WebAssembly/WASI/blob/main/design/application-abi.md
    # For a command we always want EXIT_RUNTIME=1
    # For a reactor we always want EXIT_RUNTIME=0
    if 'EXIT_RUNTIME' in user_settings:
      exit_with_error('explicitly setting EXIT_RUNTIME not compatible with STANDALONE_WASM.  EXIT_RUNTIME will always be True for programs (with a main function) and False for reactors (not main function).')
    settings.EXIT_RUNTIME = settings.EXPECT_MAIN
    settings.IGNORE_MISSING_MAIN = 0
    # the wasm must be runnable without the JS, so there cannot be anything that
    # requires JS legalization
    default_setting('LEGALIZE_JS_FFI', 0)
    if 'MEMORY_GROWTH_LINEAR_STEP' in user_settings:
      exit_with_error('MEMORY_GROWTH_LINEAR_STEP is not compatible with STANDALONE_WASM')
    if 'MEMORY_GROWTH_GEOMETRIC_CAP' in user_settings:
      exit_with_error('MEMORY_GROWTH_GEOMETRIC_CAP is not compatible with STANDALONE_WASM')

  # Note the exports the user requested
  building.user_requested_exports.update(settings.EXPORTED_FUNCTIONS)

  if '_main' in settings.EXPORTED_FUNCTIONS or 'main' in settings.EXPORT_IF_DEFINED:
    settings.EXPORT_IF_DEFINED.append('__main_argc_argv')
  elif settings.ASSERTIONS and not settings.STANDALONE_WASM and not options.no_entry:
    # In debug builds when `main` is not explicitly requested as an
    # export we still add it to EXPORT_IF_DEFINED so that we can warn
    # users who forget to explicitly export `main`.
    # See other.test_warn_unexported_main.
    # This is not needed in STANDALONE_WASM mode since we export _start
    # (unconditionally) rather than main.
    settings.EXPORT_IF_DEFINED += ['main', '__main_argc_argv']

  if settings.ASSERTIONS:
    # Exceptions are thrown with a stack trace by default when ASSERTIONS is
    # set and when building with either -fexceptions or -fwasm-exceptions.
    if 'EXCEPTION_STACK_TRACES' in user_settings and not settings.EXCEPTION_STACK_TRACES:
      exit_with_error('EXCEPTION_STACK_TRACES cannot be disabled when ASSERTIONS are enabled')
    if settings.WASM_EXCEPTIONS or not settings.DISABLE_EXCEPTION_CATCHING:
      settings.EXCEPTION_STACK_TRACES = 1

    # -sASSERTIONS implies basic stack overflow checks, and ASSERTIONS=2
    # implies full stack overflow checks. However, we don't set this default in
    # PURE_WASI, or when we are linking without standard libraries because
    # STACK_OVERFLOW_CHECK depends on emscripten_stack_get_end which is defined
    # in libcompiler-rt.
    if not settings.PURE_WASI and not options.nostdlib and not options.nodefaultlibs:
      default_setting('STACK_OVERFLOW_CHECK', max(settings.ASSERTIONS, settings.STACK_OVERFLOW_CHECK))

  # For users that opt out of WARN_ON_UNDEFINED_SYMBOLS we assume they also
  # want to opt out of ERROR_ON_UNDEFINED_SYMBOLS.
  if user_settings.get('WARN_ON_UNDEFINED_SYMBOLS') == '0':
    default_setting('ERROR_ON_UNDEFINED_SYMBOLS', 0)

  # It is unlikely that developers targeting "native web" APIs with MINIMAL_RUNTIME need
  # errno support by default.
  if settings.MINIMAL_RUNTIME:
    # Require explicit -lfoo.js flags to link with JS libraries.
    default_setting('AUTO_JS_LIBRARIES', 0)
    # When using MINIMAL_RUNTIME, symbols should only be exported if requested.
    default_setting('EXPORT_KEEPALIVE', 0)

  if settings.EXPORT_ES6 and not settings.MODULARIZE:
    # EXPORT_ES6 requires output to be a module
    if 'MODULARIZE' in user_settings:
      exit_with_error('EXPORT_ES6 requires MODULARIZE to be set')
    settings.MODULARIZE = 1

  if options.oformat == OFormat.HTML:
    if not options.shell_html:
      # Minimal runtime uses a different default shell file
      if settings.MINIMAL_RUNTIME:
        options.shell_html = options.shell_html = utils.path_from_root('html/shell_minimal_runtime.html')
      else:
        options.shell_html = DEFAULT_SHELL_HTML

    if options.shell_html == DEFAULT_SHELL_HTML:
      settings.EXPORTED_RUNTIME_METHODS.append('requestFullscreen')
  elif options.shell_html:
    diagnostics.warning('unused-command-line-argument', '--shell-file ignored when not generating html output')

  if settings.STRICT:
    if not settings.MODULARIZE:
      default_setting('STRICT_JS', 1)
    default_setting('DEFAULT_TO_CXX', 0)
    default_setting('IGNORE_MISSING_MAIN', 0)
    default_setting('AUTO_NATIVE_LIBRARIES', 0)
    if settings.MAIN_MODULE != 1:
      # These two settings cannot be disabled with MAIN_MODULE=1 because all symbols
      # are needed in this mode.
      default_setting('AUTO_JS_LIBRARIES', 0)
      default_setting('ALLOW_UNIMPLEMENTED_SYSCALLS', 0)
    limit_incoming_module_api()

  for prop in settings.INCOMING_MODULE_JS_API:
    if prop not in settings.ALL_INCOMING_MODULE_JS_API:
      diagnostics.warning('unused-command-line-argument', f'invalid entry in INCOMING_MODULE_JS_API: {prop}')

  settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.append('$wasmMemory')

  if 'noExitRuntime' in settings.INCOMING_MODULE_JS_API:
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.append('$noExitRuntime')

  # Default to TEXTDECODER=2 (always use TextDecoder to decode UTF-8 strings)
  # in -Oz builds, since custom decoder for UTF-8 takes up space.
  # When supporting shell environments, do not do this as TextDecoder is not
  # widely supported there.
  # In Audio Worklets TextDecoder API is intentionally not exposed
  # (https://github.com/WebAudio/web-audio-api/issues/2499) so we also need to
  # keep the JavaScript-based fallback.
  if settings.SHRINK_LEVEL >= 2 and not settings.ENVIRONMENT_MAY_BE_AUDIO_WORKLET and \
     not settings.ENVIRONMENT_MAY_BE_SHELL:
    default_setting('TEXTDECODER', 2)

  # If set to 1, we will run the autodebugger (the automatic debugging tool, see
  # tools/autodebugger).  Note that this will disable inclusion of libraries. This
  # is useful because including dlmalloc makes it hard to compare native and js
  # builds
  if os.environ.get('EMCC_AUTODEBUG'):
    settings.AUTODEBUG = 1

  # Use settings

  if settings.JS_MATH and settings.MAIN_MODULE == 1:
    # MODULE_MODULE=1 adds`--whole-archive` around all the system libraries which
    # results in duplicate math symbols when JS_MATH is used.
    exit_with_error('JS_MATH is not compatible with dynamic linking (MAIN_MODULE=1)')

  if settings.WASM == 2 and settings.SINGLE_FILE:
    exit_with_error('cannot have both WASM=2 and SINGLE_FILE enabled at the same time')

  if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and options.oformat != OFormat.HTML:
    exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION is only compatible with html output')

  if settings.MINIMAL_RUNTIME_STREAMING_WASM_INSTANTIATION and not settings.MINIMAL_RUNTIME:
    exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_INSTANTIATION requires MINIMAL_RUNTIME')

  if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and not settings.MINIMAL_RUNTIME:
    exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION requires MINIMAL_RUNTIME')

  if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and options.oformat != OFormat.HTML:
    exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION is only compatible with html output')

  if options.use_closure_compiler:
    settings.USE_CLOSURE_COMPILER = 1

  if 'CLOSURE_WARNINGS' in user_settings:
    if settings.CLOSURE_WARNINGS not in ['quiet', 'warn', 'error']:
      exit_with_error('invalid option -sCLOSURE_WARNINGS=%s specified! Allowed values are "quiet", "warn" or "error".' % settings.CLOSURE_WARNINGS)
    closure_warnings = diagnostics.manager.warnings['closure']
    if settings.CLOSURE_WARNINGS == 'error':
      closure_warnings['error'] = True
      closure_warnings['enabled'] = True
    elif settings.CLOSURE_WARNINGS == 'warn':
      closure_warnings['error'] = False
      closure_warnings['enabled'] = True
    elif settings.CLOSURE_WARNINGS == 'quiet':
      closure_warnings['error'] = False
      closure_warnings['enabled'] = False

  if not settings.MINIMAL_RUNTIME:
    if not settings.BOOTSTRAPPING_STRUCT_INFO:
      if settings.DYNCALLS:
        # Include dynCall() function by default in DYNCALLS builds in classic runtime; in MINIMAL_RUNTIME, must add this explicitly.
        settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$dynCall']

      if settings.ASSERTIONS:
        # "checkUnflushedContent()" and "missingLibrarySymbol()" depend on warnOnce
        settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$warnOnce']

      settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$getValue', '$setValue']

    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$ExitStatus']

    # Certain configurations require the removeRunDependency/addRunDependency system.
    if settings.LOAD_SOURCE_MAP or (settings.WASM_ASYNC_COMPILATION and not settings.MODULARIZE):
      settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$addRunDependency', '$removeRunDependency']

  if settings.ABORT_ON_WASM_EXCEPTIONS or settings.SPLIT_MODULE:
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$wasmTable']

  if settings.MAIN_MODULE:
    assert not settings.SIDE_MODULE
    if settings.MAIN_MODULE == 1:
      settings.INCLUDE_FULL_LIBRARY = 1
    # Called from preamble.js once the main module is instantiated.
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$loadDylibs']
    if not settings.RELOCATABLE:
      settings.REQUIRED_EXPORTS += ['__stack_pointer']

  if settings.MAIN_MODULE == 1 or settings.SIDE_MODULE == 1:
    settings.LINKABLE = 1

  if settings.LINKABLE and settings.USER_EXPORTS:
    diagnostics.warning('unused-command-line-argument', 'EXPORTED_FUNCTIONS is not valid with LINKABLE set (normally due to SIDE_MODULE=1/MAIN_MODULE=1) since all functions are exported this mode.  To export only a subset use SIDE_MODULE=2/MAIN_MODULE=2')

  if settings.MAIN_MODULE:
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += [
      '$getDylinkMetadata',
      '$mergeLibSymbols',
    ]

  if settings.PTHREADS:
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += [
      '$registerTLSInit',
    ]

  if settings.MAIN_MODULE or settings.RELOCATABLE:
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += [
      '$reportUndefinedSymbols',
      '$relocateExports',
      '$GOTHandler',
    ]
    # shared modules need memory utilities to allocate their memory
    settings.ALLOW_TABLE_GROWTH = 1

  # various settings require sbrk() access
  if settings.DETERMINISTIC or \
     settings.EMSCRIPTEN_TRACING or \
     settings.SAFE_HEAP or \
     settings.MEMORYPROFILER:
    settings.REQUIRED_EXPORTS += ['sbrk']

  if settings.MEMORYPROFILER:
    settings.REQUIRED_EXPORTS += ['__heap_base',
                                  'emscripten_stack_get_base',
                                  'emscripten_stack_get_end',
                                  'emscripten_stack_get_current']

  settings.ASYNCIFY_ADD = unmangle_symbols_from_cmdline(settings.ASYNCIFY_ADD)
  settings.ASYNCIFY_REMOVE = unmangle_symbols_from_cmdline(settings.ASYNCIFY_REMOVE)
  settings.ASYNCIFY_ONLY = unmangle_symbols_from_cmdline(settings.ASYNCIFY_ONLY)

  if settings.EMULATE_FUNCTION_POINTER_CASTS:
    # Emulated casts forces a wasm ABI of (i64, i64, ...) in the table, which
    # means all table functions are illegal for JS to call directly. Use
    # dyncalls which call into the wasm, which then does an indirect call.
    settings.DYNCALLS = 1

  if options.oformat != OFormat.OBJECT and final_suffix in ('.o', '.bc', '.so', '.dylib') and not settings.SIDE_MODULE:
    diagnostics.warning('emcc', 'object file output extension (%s) used for non-object output.  If you meant to build an object file please use `-c, `-r`, or `-shared`' % final_suffix)

  if settings.SUPPORT_BIG_ENDIAN:
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += [
      '$LE_HEAP_STORE_U16',
      '$LE_HEAP_STORE_I16',
      '$LE_HEAP_STORE_U32',
      '$LE_HEAP_STORE_I32',
      '$LE_HEAP_STORE_U64',
      '$LE_HEAP_STORE_I64',
      '$LE_HEAP_STORE_F32',
      '$LE_HEAP_STORE_F64',
      '$LE_HEAP_LOAD_U16',
      '$LE_HEAP_LOAD_I16',
      '$LE_HEAP_LOAD_U32',
      '$LE_HEAP_LOAD_I32',
      '$LE_HEAP_LOAD_U64',
      '$LE_HEAP_LOAD_I64',
      '$LE_HEAP_LOAD_F32',
      '$LE_HEAP_LOAD_F64',
      '$LE_ATOMICS_NATIVE_BYTE_ORDER',
      '$LE_ATOMICS_ADD',
      '$LE_ATOMICS_AND',
      '$LE_ATOMICS_COMPAREEXCHANGE',
      '$LE_ATOMICS_EXCHANGE',
      '$LE_ATOMICS_ISLOCKFREE',
      '$LE_ATOMICS_LOAD',
      '$LE_ATOMICS_NOTIFY',
      '$LE_ATOMICS_OR',
      '$LE_ATOMICS_STORE',
      '$LE_ATOMICS_SUB',
      '$LE_ATOMICS_WAIT',
      '$LE_ATOMICS_WAITASYNC',
      '$LE_ATOMICS_XOR',
    ]

  if settings.RUNTIME_DEBUG or settings.ASSERTIONS or settings.STACK_OVERFLOW_CHECK or settings.PTHREADS_PROFILING or settings.GL_ASSERTIONS:
    # Lots of code in debug/assertion blocks uses ptrToString.
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$ptrToString']

  if settings.STACK_OVERFLOW_CHECK:
    settings.REQUIRED_EXPORTS += [
      'emscripten_stack_get_end',
      'emscripten_stack_get_free',
      'emscripten_stack_get_base',
      'emscripten_stack_get_current',
    ]

    # We call one of these two functions during startup which caches the stack limits
    # in wasm globals allowing get_base/get_free to be super fast.
    # See compiler-rt/stack_limits.S.
    if settings.RELOCATABLE:
      settings.REQUIRED_EXPORTS += ['emscripten_stack_set_limits']
    else:
      settings.REQUIRED_EXPORTS += ['emscripten_stack_init']

  if settings.STACK_OVERFLOW_CHECK >= 2:
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$setStackLimits']

  check_browser_versions()

  if settings.MIN_NODE_VERSION >= 150000:
    default_setting('NODEJS_CATCH_REJECTION', 0)

  # Do not catch rejections or exits in modularize mode, as these options
  # are for use when running emscripten modules standalone
  # see https://github.com/emscripten-core/emscripten/issues/18723#issuecomment-1429236996
  if settings.MODULARIZE:
    default_setting('NODEJS_CATCH_REJECTION', 0)

  if settings.POLYFILL:
    # Emscripten requires certain ES6+ constructs by default in library code
    # - (various ES6 operators available in all browsers listed below)
    # - https://caniuse.com/mdn-javascript_operators_nullish_coalescing:
    #                                          FF:72 CHROME:80 SAFARI:13.1 NODE:14
    # - https://caniuse.com/mdn-javascript_operators_optional_chaining:
    #                                          FF:74 CHROME:80 SAFARI:13.1 NODE:14
    # - https://caniuse.com/mdn-javascript_operators_logical_or_assignment:
    #                                          FF:79 CHROME:85 SAFARI:14 NODE:16
    # Taking the highest requirements gives is our minimum:
    #                             Max Version: FF:79 CHROME:85 SAFARI:14 NODE:16
    # TODO: replace this with feature matrix in the future.
    settings.TRANSPILE = (settings.MIN_FIREFOX_VERSION < 79 or
                          settings.MIN_CHROME_VERSION < 85 or
                          settings.MIN_SAFARI_VERSION < 140000 or
                          settings.MIN_NODE_VERSION < 160000)

  if settings.STB_IMAGE:
    settings.EXPORTED_FUNCTIONS += ['_stbi_load', '_stbi_load_from_memory', '_stbi_image_free']

  if settings.USE_WEBGL2:
    settings.MAX_WEBGL_VERSION = 2

  # MIN_WEBGL_VERSION=2 implies MAX_WEBGL_VERSION=2
  if settings.MIN_WEBGL_VERSION == 2:
    default_setting('MAX_WEBGL_VERSION', 2)

  if settings.MIN_WEBGL_VERSION > settings.MAX_WEBGL_VERSION:
    exit_with_error('MIN_WEBGL_VERSION must be smaller or equal to MAX_WEBGL_VERSION!')

  if options.use_preload_plugins or len(options.preload_files) or len(options.embed_files):
    if settings.NODERAWFS:
      exit_with_error('--preload-file and --embed-file cannot be used with NODERAWFS which disables virtual filesystem')
    # if we include any files, or intend to use preload plugins, then we definitely need filesystem support
    settings.FORCE_FILESYSTEM = 1

  if options.preload_files:
    # File preloading uses `Module['preRun']`.
    settings.INCOMING_MODULE_JS_API.append('preRun')

  if settings.FORCE_FILESYSTEM and not settings.FILESYSTEM:
    exit_with_error('`-sFORCE_FILESYSTEM` cannot be used with `-sFILESYSTEM=0`')

  if settings.WASMFS:
    settings.FILESYSTEM = 1
    settings.SYSCALLS_REQUIRE_FILESYSTEM = 0
    add_system_js_lib('libwasmfs.js')
    if settings.ASSERTIONS:
      # used in assertion checks for unflushed content
      settings.REQUIRED_EXPORTS += ['wasmfs_flush']
    if settings.FORCE_FILESYSTEM or settings.INCLUDE_FULL_LIBRARY:
      # Add exports for the JS API. Like the old JS FS, WasmFS by default
      # includes just what JS parts it actually needs, and FORCE_FILESYSTEM is
      # required to force all of it to be included if the user wants to use the
      # JS API directly. (INCLUDE_FULL_LIBRARY also causes this code to be
      # included, as the entire JS library can refer to things that require
      # these exports.)
      settings.REQUIRED_EXPORTS += [
        'emscripten_builtin_memalign',
        'wasmfs_create_file',
        'wasmfs_unmount',
        '_wasmfs_mount',
        '_wasmfs_read_file',
        '_wasmfs_write_file',
        '_wasmfs_open',
        '_wasmfs_close',
        '_wasmfs_write',
        '_wasmfs_pwrite',
        '_wasmfs_rename',
        '_wasmfs_mkdir',
        '_wasmfs_unlink',
        '_wasmfs_chdir',
        '_wasmfs_mknod',
        '_wasmfs_rmdir',
        '_wasmfs_mmap',
        '_wasmfs_munmap',
        '_wasmfs_msync',
        '_wasmfs_read',
        '_wasmfs_pread',
        '_wasmfs_symlink',
        '_wasmfs_truncate',
        '_wasmfs_ftruncate',
        '_wasmfs_stat',
        '_wasmfs_lstat',
        '_wasmfs_chmod',
        '_wasmfs_fchmod',
        '_wasmfs_lchmod',
        '_wasmfs_utime',
        '_wasmfs_llseek',
        '_wasmfs_identify',
        '_wasmfs_readlink',
        '_wasmfs_readdir_start',
        '_wasmfs_readdir_get',
        '_wasmfs_readdir_finish',
        '_wasmfs_get_cwd',
      ]

  if settings.FULL_ES3:
    settings.FULL_ES2 = 1
    settings.MAX_WEBGL_VERSION = max(2, settings.MAX_WEBGL_VERSION)

  if settings.MAIN_READS_PARAMS and not settings.STANDALONE_WASM:
    # callMain depends on _emscripten_stack_alloc
    settings.REQUIRED_EXPORTS += ['_emscripten_stack_alloc']

  if settings.SUPPORT_LONGJMP == 'emscripten' or not settings.DISABLE_EXCEPTION_CATCHING:
    # make_invoke depends on stackSave and stackRestore
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$stackSave', '$stackRestore']

  if settings.SIDE_MODULE and 'GLOBAL_BASE' in user_settings:
    diagnostics.warning('unused-command-line-argument', 'GLOBAL_BASE is not compatible with SIDE_MODULE')

  if options.use_preload_plugins:
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$Browser']

  if not settings.BOOTSTRAPPING_STRUCT_INFO:
    if settings.DYNAMIC_EXECUTION == 2 and not settings.MINIMAL_RUNTIME:
      # Used by makeEval in the DYNAMIC_EXECUTION == 2 case
      settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$stackTrace']

    if not settings.STANDALONE_WASM and (settings.EXIT_RUNTIME or settings.ASSERTIONS):
      # to flush streams on FS exit, we need to be able to call fflush
      # we only include it if the runtime is exitable, or when ASSERTIONS
      # (ASSERTIONS will check that streams do not need to be flushed,
      # helping people see when they should have enabled EXIT_RUNTIME)
      settings.EXPORT_IF_DEFINED += ['fflush']

  if settings.SAFE_HEAP:
    # SAFE_HEAP check includes calling emscripten_get_sbrk_ptr() from wasm
    settings.REQUIRED_EXPORTS += ['emscripten_get_sbrk_ptr', 'emscripten_stack_get_base']

  if not settings.DECLARE_ASM_MODULE_EXPORTS:
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$assignWasmExports']

  if settings.ALLOW_MEMORY_GROWTH:
    # Setting ALLOW_MEMORY_GROWTH turns off ABORTING_MALLOC, as in that mode we default to
    # the behavior of trying to grow and returning 0 from malloc on failure, like
    # a standard system would. However, if the user sets the flag it
    # overrides that.
    default_setting('ABORTING_MALLOC', 0)

  if settings.EMBIND:
    # Workaround for embind+LTO issue:
    # https://github.com/emscripten-core/emscripten/issues/21653
    settings.REQUIRED_EXPORTS.append('__getTypeName')
    if settings.PTHREADS or settings.WASM_WORKERS:
      settings.REQUIRED_EXPORTS.append('_embind_initialize_bindings')
    # Needed to assign the embind exports to the ES exports.
    if settings.MODULARIZE == 'instance':
      settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$addOnPostCtor']

  if options.emit_tsd:
    settings.EMIT_TSD = True

  if settings.PTHREADS:
    setup_pthreads()
    add_system_js_lib('libpthread.js')
    if settings.PROXY_TO_PTHREAD:
      settings.PTHREAD_POOL_SIZE_STRICT = 0
      settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$runtimeKeepalivePush']
  else:
    if settings.PROXY_TO_PTHREAD:
      exit_with_error('-sPROXY_TO_PTHREAD requires -pthread to work!')
    add_system_js_lib('libpthread_stub.js')

  if settings.MEMORY64:
    # Any "pointers" passed to JS will now be i64's, in both modes.
    settings.WASM_BIGINT = 1

  if settings.MEMORY64 and settings.RELOCATABLE:
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.append('__table_base32')

  if settings.WASM_WORKERS:
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$_wasmWorkerInitializeRuntime']
    add_system_js_lib('libwasm_worker.js')

  # Set min browser versions based on certain settings such as WASM_BIGINT,
  # PTHREADS, AUDIO_WORKLET
  # Such setting must be set before this point
  feature_matrix.apply_min_browser_versions()

  # TODO(sbc): Find make a generic way to expose the feature matrix to JS
  # compiler rather then adding them all ad-hoc as internal settings
  settings.SUPPORTS_PROMISE_ANY = feature_matrix.caniuse(Feature.PROMISE_ANY)
  default_setting('WASM_BIGINT', feature_matrix.caniuse(Feature.JS_BIGINT_INTEGRATION))

  if settings.AUDIO_WORKLET:
    add_system_js_lib('libwebaudio.js')
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.append('$getWasmTableEntry')

  if not settings.MINIMAL_RUNTIME:
    if 'preRun' in settings.INCOMING_MODULE_JS_API:
      settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.append('$addOnPreRun')
    if 'postRun' in settings.INCOMING_MODULE_JS_API:
      settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.append('$addOnPostRun')

  if settings.FORCE_FILESYSTEM and not settings.MINIMAL_RUNTIME:
    # when the filesystem is forced, we export by default methods that filesystem usage
    # may need, including filesystem usage from standalone file packager output (i.e.
    # file packages not built together with emcc, but that are loaded at runtime
    # separately, and they need emcc's output to contain the support they need)
    settings.EXPORTED_RUNTIME_METHODS += [
      'FS_createPath',
      'FS_createDataFile',
      'FS_preloadFile',
      'FS_unlink',
    ]
    if not settings.WASMFS:
      # The old FS has some functionality that WasmFS lacks.
      settings.EXPORTED_RUNTIME_METHODS += [
        'FS_createLazyFile',
        'FS_createDevice',
      ]

    settings.EXPORTED_RUNTIME_METHODS += [
      'addRunDependency',
      'removeRunDependency',
    ]

  if settings.PTHREADS or settings.WASM_WORKERS or settings.RELOCATABLE:
    settings.IMPORTED_MEMORY = 1

  set_initial_memory()

  # When not declaring wasm module exports in outer scope one by one, disable minifying
  # wasm module export names so that the names can be passed directly to the outer scope.
  # Also, if using libexports.js API, disable minification so that the feature can work.
  if not settings.DECLARE_ASM_MODULE_EXPORTS or '-lexports.js' in linker_args:
    settings.MINIFY_WASM_EXPORT_NAMES = 0

  # Enable minification of wasm imports and exports when appropriate, if we
  # are emitting an optimized JS+wasm combo (then the JS knows how to load the minified names).
  # Things that process the JS after this operation would be done must disable this.
  # JSPI does not support this optimization yet as it has a hardcoded check for 'main' as an
  # export name. TODO
  if will_metadce() and \
      settings.OPT_LEVEL >= 2 and \
      settings.DEBUG_LEVEL <= 2 and \
      options.oformat not in (OFormat.WASM, OFormat.BARE) and \
      settings.ASYNCIFY != 2 and \
      not settings.LINKABLE and \
      not settings.STANDALONE_WASM and \
      not settings.AUTODEBUG and \
      not settings.ASSERTIONS and \
      not settings.RELOCATABLE and \
      not settings.MAIN_MODULE and \
          settings.MINIFY_WASM_EXPORT_NAMES:
    settings.MINIFY_WASM_IMPORTS_AND_EXPORTS = 1
    settings.MINIFY_WASM_IMPORTED_MODULES = 1

  if settings.WASM_BIGINT:
    settings.LEGALIZE_JS_FFI = 0

  if settings.SINGLE_FILE and settings.GENERATE_SOURCE_MAP:
    diagnostics.warning('emcc', 'SINGLE_FILE disables source map support (which requires a .map file)')
    settings.GENERATE_SOURCE_MAP = 0

  if options.use_closure_compiler == 2 and not settings.WASM2JS:
    exit_with_error('closure compiler mode 2 assumes the code is asm.js, so not meaningful for wasm')

  if settings.AUTODEBUG:
    settings.REQUIRED_EXPORTS += ['_emscripten_tempret_set']

  if settings.LEGALIZE_JS_FFI:
    settings.REQUIRED_EXPORTS += ['__get_temp_ret', '__set_temp_ret']

  if settings.SPLIT_MODULE:
    settings.INCOMING_MODULE_JS_API += ['loadSplitModule']
    if settings.ASYNCIFY == 2:
      settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['_load_secondary_module']

  # wasm side modules have suffix .wasm
  if settings.SIDE_MODULE and utils.suffix(target) in ('.js', '.mjs'):
    diagnostics.warning('emcc', 'JavaScript output suffix requested, but wasm side modules are just wasm files; emitting only a .wasm, no .js')

  if options.sanitize:
    setup_sanitizers(options)

  if settings.USE_ASAN or settings.SAFE_HEAP:
    # ASan and SAFE_HEAP check address 0 themselves
    settings.CHECK_NULL_WRITES = 0

  if 'GLOBAL_BASE' not in user_settings and not settings.SHRINK_LEVEL and not settings.OPT_LEVEL and not settings.USE_ASAN:
    # When optimizing for size it helps to put static data first before
    # the stack (since this makes instructions for accessing this data
    # use a smaller LEB encoding).
    # However, for debugability is better to have the stack come first
    # (because stack overflows will trap rather than corrupting data).
    settings.STACK_FIRST = True

  if '--stack-first' in linker_args:
    settings.STACK_FIRST = True
    if settings.USE_ASAN:
      exit_with_error('--stack-first is not compatible with asan')
    if 'GLOBAL_BASE' in user_settings:
      exit_with_error('--stack-first is not compatible with -sGLOBAL_BASE')

  set_max_memory()

  # check if we can address the 2GB mark and higher.
  if not settings.MEMORY64 and settings.MAXIMUM_MEMORY > 2 * 1024 * 1024 * 1024:
    settings.CAN_ADDRESS_2GB = 1

  if settings.MAX_WEBGL_VERSION >= 2:
    settings.WEBGL_USE_GARBAGE_FREE_APIS = 1
    # Some browsers have issues using the WebGL2 garbage-free APIs when the
    # memory offsets are over 2^31 or 2^32
    # For firefox see: https://bugzil.la/1838218
    if settings.MIN_FIREFOX_VERSION != feature_matrix.UNSUPPORTED and settings.MAXIMUM_MEMORY > 2 ** 31:
      settings.WEBGL_USE_GARBAGE_FREE_APIS = 0
    # For chrome see: https://crbug.com/324992397
    if settings.MIN_CHROME_VERSION != feature_matrix.UNSUPPORTED and settings.MEMORY64 and settings.MAXIMUM_MEMORY > 2 ** 32:
      settings.WEBGL_USE_GARBAGE_FREE_APIS = 0
    if settings.WEBGL_USE_GARBAGE_FREE_APIS and settings.MIN_WEBGL_VERSION >= 2:
      settings.INCLUDE_WEBGL1_FALLBACK = 0

  if settings.MINIMAL_RUNTIME:
    if settings.EXIT_RUNTIME:
      settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['proc_exit', '$callRuntimeCallbacks']
  else:
    # MINIMAL_RUNTIME only needs callRuntimeCallbacks in certain cases, but the normal runtime
    # always does.
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$callRuntimeCallbacks']

  if settings.EXIT_RUNTIME and not settings.STANDALONE_WASM:
    # Internal function implemented in musl that calls any functions registered
    # via `atexit` et al.  With STANDALONE_WASM this is all taken care of via
    # _start and exit handling in musl, but with the normal emscripten ABI we
    # need to be able to call these explicitly.
    settings.REQUIRED_EXPORTS += ['__funcs_on_exit']

  if settings.BUILD_AS_WORKER:
    # The worker code in src/build_as_worker.js depends on realloc
    settings.REQUIRED_EXPORTS += ['realloc']
    options.post_js.append(utils.path_from_root('src/build_as_worker.js'))

  if not settings.DISABLE_EXCEPTION_CATCHING:
    settings.REQUIRED_EXPORTS += [
      # For normal builds the entries in deps_info.py are enough to include
      # these symbols whenever __cxa_find_matching_catch_* functions are
      # found.  However, under LTO these symbols don't exist prior to linking
      # so we include then unconditionally when exceptions are enabled.
      '__cxa_can_catch',

      # __cxa_begin_catch depends on this but we can't use deps info in this
      # case because that only works for user-level code, and __cxa_begin_catch
      # can be used by the standard library.
      '__cxa_increment_exception_refcount',
      # Same for __cxa_end_catch
      '__cxa_decrement_exception_refcount',

      # Emscripten exception handling can generate invoke calls, and they call
      # setThrew(). We cannot handle this using deps_info as the invokes are not
      # emitted because of library function usage, but by codegen itself.
      'setThrew',
      '__cxa_free_exception',
    ]

  if settings.ASYNCIFY:
    if not settings.ASYNCIFY_IGNORE_INDIRECT:
      # if we are not ignoring indirect calls, then we must treat invoke_* as if
      # they are indirect calls, since that is what they do - we can't see their
      # targets statically.
      settings.ASYNCIFY_IMPORTS += ['invoke_*']
    # add the default imports
    settings.ASYNCIFY_IMPORTS += DEFAULT_ASYNCIFY_IMPORTS
    # add the default exports (only used for JSPI)
    settings.ASYNCIFY_EXPORTS += DEFAULT_ASYNCIFY_EXPORTS

    # return the full import name, including module. The name may
    # already have a module prefix; if not, we assume it is "env".
    def get_full_import_name(name):
      if '.' in name:
        return name
      return 'env.' + name

    settings.ASYNCIFY_IMPORTS = [get_full_import_name(i) for i in settings.ASYNCIFY_IMPORTS]

    if settings.ASYNCIFY == 2:
      diagnostics.warning('experimental', '-sJSPI (ASYNCIFY=2) is still experimental')

  if settings.WASM2JS:
    if settings.GENERATE_SOURCE_MAP:
      exit_with_error('wasm2js does not support source maps yet (debug in wasm for now)')
    if settings.MEMORY64:
      exit_with_error('wasm2js does not support MEMORY64')
    if settings.WASM_BIGINT:
      exit_with_error('wasm2js does not support WASM_BIGINT')
    if settings.CAN_ADDRESS_2GB:
      exit_with_error('wasm2js does not support >2gb address space')

  if settings.NODE_CODE_CACHING:
    if settings.WASM_ASYNC_COMPILATION:
      exit_with_error('NODE_CODE_CACHING requires sync compilation (WASM_ASYNC_COMPILATION=0)')
    if not settings.ENVIRONMENT_MAY_BE_NODE:
      exit_with_error('NODE_CODE_CACHING only works in node, but target environments do not include it')
    if settings.SINGLE_FILE:
      exit_with_error('NODE_CODE_CACHING saves a file on the side and is not compatible with SINGLE_FILE')

  if not js_manipulation.isidentifier(settings.EXPORT_NAME):
    exit_with_error(f'EXPORT_NAME is not a valid JS identifier: `{settings.EXPORT_NAME}`')

  if settings.EMSCRIPTEN_TRACING:
    add_system_js_lib('libtrace.js')
    if settings.ALLOW_MEMORY_GROWTH:
      settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['emscripten_trace_report_memory_layout']
      settings.REQUIRED_EXPORTS += ['emscripten_stack_get_current',
                                    'emscripten_stack_get_base',
                                    'emscripten_stack_get_end']

  settings.EMSCRIPTEN_VERSION = utils.EMSCRIPTEN_VERSION
  if settings.RETAIN_COMPILER_SETTINGS:
    settings.PUBLIC_SETTINGS = [k for k in settings.attrs.keys() if k not in settings.internal_settings]
    # Also include EMSCRIPTEN_VERSION from the internal settings since there are
    # known usage of this with `emscripten_get_compiler_setting`.
    settings.PUBLIC_SETTINGS.append('EMSCRIPTEN_VERSION')
  settings.SOURCE_MAP_BASE = options.source_map_base or ''

  settings.LINK_AS_CXX = (shared.run_via_emxx or settings.DEFAULT_TO_CXX) and not options.nostdlibxx

  # WASMFS itself is written in C++, and needs C++ standard libraries
  if settings.WASMFS:
    settings.LINK_AS_CXX = True

  # Some settings make no sense when not linking as C++
  if not settings.LINK_AS_CXX:
    cxx_only_settings = [
      'EXCEPTION_DEBUG',
      'DISABLE_EXCEPTION_CATCHING',
      'EXCEPTION_CATCHING_ALLOWED',
      'DISABLE_EXCEPTION_THROWING',
    ]
    for setting in cxx_only_settings:
      if setting in user_settings:
        diagnostics.warning('linkflags', 'setting `%s` is not meaningful unless linking as C++', setting)

  if settings.WASM_EXCEPTIONS:
    settings.REQUIRED_EXPORTS += ['__trap']

  if settings.EXCEPTION_STACK_TRACES:
    # If the user explicitly gave EXCEPTION_STACK_TRACES=1 without enabling EH,
    # errors out.
    if settings.DISABLE_EXCEPTION_CATCHING and not settings.WASM_EXCEPTIONS:
      exit_with_error('EXCEPTION_STACK_TRACES requires either of -fexceptions or -fwasm-exceptions')
    # EXCEPTION_STACK_TRACES implies EXPORT_EXCEPTION_HANDLING_HELPERS
    settings.EXPORT_EXCEPTION_HANDLING_HELPERS = True

  # Make `getExceptionMessage` and other necessary functions available for use.
  if settings.EXPORT_EXCEPTION_HANDLING_HELPERS:
    # If the user explicitly gave EXPORT_EXCEPTION_HANDLING_HELPERS=1 without
    # enabling EH, errors out.
    if settings.DISABLE_EXCEPTION_CATCHING and not settings.WASM_EXCEPTIONS:
      exit_with_error('EXPORT_EXCEPTION_HANDLING_HELPERS requires either of -fexceptions or -fwasm-exceptions')
    # We also export refcount incrementing and decrementing functions because if
    # you catch an exception from JS, you may need to manipulate the refcount
    # manually to avoid memory leaks.  See test_EXPORT_EXCEPTION_HANDLING_HELPERS
    # in test/test_core.py for an example usage.
    settings.EXPORTED_FUNCTIONS += ['getExceptionMessage', 'incrementExceptionRefcount', 'decrementExceptionRefcount']
    if settings.WASM_EXCEPTIONS:
      settings.REQUIRED_EXPORTS += ['__cpp_exception']

  if settings.SIDE_MODULE:
    # For side modules, we ignore all REQUIRED_EXPORTS that might have been added above.
    # They all come from either libc or compiler-rt.  The exception is __wasm_call_ctors
    # and _emscripten_tls_init which are per-module exports.
    settings.REQUIRED_EXPORTS.clear()

  if not settings.STANDALONE_WASM:
    # in standalone mode, crt1 will call the constructors from inside the wasm
    settings.REQUIRED_EXPORTS.append('__wasm_call_ctors')
  if settings.PTHREADS:
    settings.REQUIRED_EXPORTS.append('_emscripten_tls_init')

  if settings.NODERAWFS:
    default_setting('NODE_HOST_ENV', 1)

  if not settings.ENVIRONMENT_MAY_BE_NODE:
    # Node-specific settings only make sense if ENVIRONMENT_MAY_BE_NODE
    if settings.NODERAWFS:
      diagnostics.warning('unused-command-line-argument', 'NODERAWFS ignored since `node` not in `ENVIRONMENT`')
    if settings.NODE_CODE_CACHING:
      diagnostics.warning('unused-command-line-argument', 'NODE_CODE_CACHING ignored since `node` not in `ENVIRONMENT`')
    if settings.NODEJS_CATCH_EXIT:
      diagnostics.warning('unused-command-line-argument', 'NODEJS_CATCH_EXIT ignored since `node` not in `ENVIRONMENT`')
    if settings.NODEJS_CATCH_REJECTION and 'NODEJS_CATCH_REJECTION' in user_settings:
      diagnostics.warning('unused-command-line-argument', 'NODEJS_CATCH_REJECTION ignored since `node` not in `ENVIRONMENT`')

  settings.PRE_JS_FILES = options.pre_js
  settings.POST_JS_FILES = options.post_js

  settings.MINIFY_WHITESPACE = settings.OPT_LEVEL >= 2 and settings.DEBUG_LEVEL == 0 and not options.no_minify

  # Closure might be run if we run it ourselves, or if whitespace is not being
  # minified. In the latter case we keep both whitespace and comments, and the
  # purpose of the comments might be closure compiler, so also perform all
  # adjustments necessary to ensure that works (which amounts to a few more
  # comments; adding some more of them is not an issue in such a build which
  # includes all comments and whitespace anyhow).
  if settings.USE_CLOSURE_COMPILER or not settings.MINIFY_WHITESPACE:
    settings.MAYBE_CLOSURE_COMPILER = 1

  check_settings()

  return target, wasm_target


@ToolchainProfiler.profile_block('calculate system libraries')
def phase_calculate_system_libraries(options):
  extra_files_to_link = []
  # Link in ports and system libraries, if necessary
  if not settings.SIDE_MODULE:
    # Ports are always linked into the main module, never the side module.
    extra_files_to_link += ports.get_libs(settings)
  extra_files_to_link += system_libs.calculate(options)
  return extra_files_to_link


@ToolchainProfiler.profile_block('link')
def phase_link(linker_args, wasm_target, js_syms):
  logger.debug(f'linking: {linker_args}')

  # Make a final pass over settings.EXPORTED_FUNCTIONS to remove any
  # duplication between functions added by the driver/libraries and function
  # specified by the user
  settings.EXPORTED_FUNCTIONS = dedup_list(settings.EXPORTED_FUNCTIONS)
  settings.REQUIRED_EXPORTS = dedup_list(settings.REQUIRED_EXPORTS)
  settings.EXPORT_IF_DEFINED = dedup_list(settings.EXPORT_IF_DEFINED)

  rtn = None
  if settings.LINKABLE and not settings.EXPORT_ALL:
    # In LINKABLE mode we pass `--export-dynamic` along with `--whole-archive`.  This results
    # in over 7000 exports, which cannot be distinguished from the few symbols we explicitly
    # export via EMSCRIPTEN_KEEPALIVE or EXPORTED_FUNCTIONS.
    # In order to avoid unnecessary exported symbols on the `Module` object we run the linker
    # twice in this mode:
    # 1. Without `--export-dynamic` to get the base exports
    # 2. With `--export-dynamic` to get the actual linkable Wasm binary
    # TODO(sbc): Remove this double execution of wasm-ld if we ever find a way to
    # distinguish EMSCRIPTEN_KEEPALIVE exports from `--export-dynamic` exports.
    settings.LINKABLE = False
    building.link_lld(linker_args, wasm_target, external_symbols=js_syms)
    settings.LINKABLE = True
    rtn = extract_metadata.extract_metadata(wasm_target)

  building.link_lld(linker_args, wasm_target, external_symbols=js_syms)
  return rtn


@ToolchainProfiler.profile_block('post link')
def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadata=None):
  global final_js

  target_basename = unsuffixed_basename(target)

  if options.oformat != OFormat.WASM:
    final_js = in_temp(target_basename + '.js')

  settings.TARGET_BASENAME = unsuffixed_basename(target)

  if options.oformat in (OFormat.JS, OFormat.MJS):
    js_target = target
  else:
    js_target = get_secondary_target(target, '.js')

  settings.TARGET_JS_NAME = os.path.basename(js_target)

  metadata = phase_emscript(in_wasm, wasm_target, js_syms, base_metadata)

  if settings.EMBIND_AOT:
    phase_embind_aot(options, wasm_target, js_syms)

  if options.emit_tsd:
    phase_emit_tsd(options, wasm_target, js_target, js_syms, metadata)

  if options.js_transform:
    phase_source_transforms(options)

  phase_binaryen(target, options, wasm_target)

  # If we are not emitting any JS then we are all done now
  if options.oformat != OFormat.WASM:
    phase_final_emitting(options, target, js_target, wasm_target)


@ToolchainProfiler.profile_block('emscript')
def phase_emscript(in_wasm, wasm_target, js_syms, base_metadata):
  # Emscripten
  logger.debug('emscript')

  # No need to support base64 embedding in wasm2js mode since
  # the module is already in JS format.
  if settings.SINGLE_FILE and not settings.WASM2JS:
    settings.SUPPORT_BASE64_EMBEDDING = 1
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.append('$base64Decode')

  if shared.SKIP_SUBPROCS:
    return

  metadata = emscripten.emscript(in_wasm, wasm_target, final_js, js_syms, base_metadata=base_metadata)
  save_intermediate('original')
  return metadata


def run_embind_gen(options, wasm_target, js_syms, extra_settings):
  # Save settings so they can be restored after TS generation.
  original_settings = settings.backup()
  settings.attrs.update(extra_settings)
  settings.EMBIND_GEN_MODE = True

  if settings.MAIN_MODULE:
    # Copy libraries to the temp directory so they can be used when running
    # in node.
    for f in options.input_files:
      if building.is_wasm_dylib(f):
        safe_copy(f, in_temp(''))

  # Ignore any options or settings that can conflict with running the TS
  # generation output.
  # Don't invoke the program's `main` function.
  settings.INVOKE_RUN = False
  # Ignore -sMODULARIZE which could otherwise affect how we run the module
  # to generate the bindings.
  settings.MODULARIZE = False
  # Disable ESM integration to avoid enabling the experimental feature in node.
  settings.WASM_ESM_INTEGRATION = False
  # Don't include any custom user JS or files.
  settings.PRE_JS_FILES = []
  settings.POST_JS_FILES = []
  # Force node since that is where the tool runs.
  if 'node' not in settings.ENVIRONMENT:
    settings.ENVIRONMENT.append('node')
  settings.MINIMAL_RUNTIME = 0
  # Required function to trigger TS generation.
  settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$callRuntimeCallbacks', '$addRunDependency', '$removeRunDependency']
  settings.EXPORT_ES6 = False
  # Disable proxying and thread pooling so a worker is not automatically created.
  settings.PROXY_TO_PTHREAD = False
  settings.PTHREAD_POOL_SIZE = 0
  # Assume wasm support at binding generation time
  settings.WASM2JS = 0
  # Disable minify since the binaryen pass has not been run yet to change the
  # import names.
  settings.MINIFY_WASM_IMPORTED_MODULES = False
  setup_environment_settings()
  # Use a separate Wasm file so the JS does not need to be modified after emscripten.emscript.
  settings.SINGLE_FILE = False
  # Replace embind with the TypeScript generation version.
  for i, lib in enumerate(settings.JS_LIBRARIES):
    dirname, basename = os.path.split(lib)
    if basename == 'libembind.js':
      settings.JS_LIBRARIES[i] = os.path.join(dirname, 'libembind_gen.js')
  settings.MIN_NODE_VERSION = 160000 if settings.MEMORY64 else 150000
  # The final version of the memory64 proposal is not implemented until node
  # v24, so we need to lower it away in order to execute the binary at build
  # time.
  # TODO Remove lowering when emsdk version of node is >= 24 and just require it.
  if settings.MEMORY64:
    settings.MEMORY64 = 2
  # Source maps haven't been generated yet and aren't needed to run embind_gen.
  settings.LOAD_SOURCE_MAP = 0
  outfile_js = in_temp('tsgen.js')
  # The Wasm outfile may be modified by emscripten.emscript, so use a temporary file.
  outfile_wasm = in_temp('tsgen.wasm')
  emscripten.emscript(wasm_target, outfile_wasm, outfile_js, js_syms, finalize=False)
  # Build the flags needed by Node.js to properly run the output file.
  node_args = []
  if settings.MEMORY64:
    # See comment above about lowering memory64.
    building.run_wasm_opt(outfile_wasm, outfile_wasm, ['--memory64-lowering', '--table64-lowering'])
  if settings.WASM_EXCEPTIONS:
    node_args += shared.node_exception_flags(config.NODE_JS)
  # Run the generated JS file with the proper flags to generate the TypeScript bindings.
  output_file = in_temp('embind_generated_output.js')
  shared.run_js_tool(outfile_js, [output_file], node_args)
  settings.restore(original_settings)
  return read_file(output_file)


@ToolchainProfiler.profile_block('emit tsd')
def phase_emit_tsd(options, wasm_target, js_target, js_syms, metadata):
  logger.debug('emit tsd')
  filename = options.emit_tsd
  embind_tsd = ''
  if settings.EMBIND:
    embind_tsd = run_embind_gen(options, wasm_target, js_syms, {'EMBIND_AOT': False})
  all_tsd = emscripten.create_tsd(metadata, embind_tsd)
  out_file = os.path.join(os.path.dirname(js_target), filename)
  write_file(out_file, all_tsd)


@ToolchainProfiler.profile_block('embind aot js')
def phase_embind_aot(options, wasm_target, js_syms):
  out = run_embind_gen(options, wasm_target, js_syms, {})
  if DEBUG:
    write_file(in_temp('embind_aot.json'), out)
  out = json.loads(out)
  src = read_file(final_js)
  src = do_replace(src, '<<< EMBIND_AOT_INVOKERS >>>', out['invokers'])
  if settings.MODULARIZE == 'instance':
    # Add ES module exports for the embind exports.
    decls = '\n'.join([f'export var {name};' for name in out['publicSymbols']])
    # Assign the runtime exports from Module to the ES export.
    assigns = '\n'.join([f'{name} = Module[\'{name}\'];' for name in out['publicSymbols']])
    exports = f'''
// start embind exports
function assignEmbindExports() {{ {assigns} }};
addOnPostCtor(assignEmbindExports);
{decls}
// end embind exports'''
    src += exports
  write_file(final_js, src)
  if settings.WASM_ESM_INTEGRATION:
    # With ESM integration the embind exports also need to be exported by the main file.
    settings.EXPORTED_RUNTIME_METHODS.extend(out['publicSymbols'])


# for Popen, we cannot have doublequotes, so provide functionality to
# remove them when needed.
def remove_quotes(arg):
  if isinstance(arg, list):
    return [remove_quotes(a) for a in arg]

  if arg.startswith('"') and arg.endswith('"'):
    return arg[1:-1].replace('\\"', '"')
  elif arg.startswith("'") and arg.endswith("'"):
    return arg[1:-1].replace("\\'", "'")
  else:
    return arg


@ToolchainProfiler.profile_block('source transforms')
def phase_source_transforms(options):
  # Apply a source code transformation, if requested
  global final_js
  safe_copy(final_js, final_js + '.tr.js')
  final_js += '.tr.js'
  posix = not WINDOWS
  logger.debug('applying transform: %s', options.js_transform)
  shared.check_call(remove_quotes(shlex.split(options.js_transform, posix=posix) + [os.path.abspath(final_js)]))
  save_intermediate('transformed')


# Unmangle previously mangled `import.meta` and `await import` references in
# both main code and libraries.
# See also: `preprocess` in parseTools.js.
def fix_js_mangling(js_file):
  # We don't apply these mangliings except in MODULARIZE/EXPORT_ES6 modes.
  if not settings.MODULARIZE:
    return

  src = read_file(js_file)
  write_file(js_file, src
             .replace('EMSCRIPTEN$IMPORT$META', 'import.meta')
             .replace('EMSCRIPTEN$AWAIT$IMPORT', 'await import')
             .replace('EMSCRIPTEN$AWAIT(', 'await ('))
  save_intermediate('js-mangling')


def node_detection_code():
  return "globalThis.process?.versions?.node && globalThis.process?.type != 'renderer'"


def create_esm_wrapper(wrapper_file, support_target, wasm_target):
  js_exports = building.user_requested_exports.union(settings.EXPORTED_RUNTIME_METHODS)
  js_exports = ', '.join(sorted(js_exports))

  wrapper = []
  wrapper.append('// The wasm module must be imported here first before the support file')
  wrapper.append('// in order to avoid issues with circular dependencies.')
  wrapper.append(f"import * as unused from './{settings.WASM_BINARY_FILE}';")
  support_url = f'./{os.path.basename(support_target)}'
  if js_exports:
    wrapper.append(f"export {{ default, {js_exports} }} from '{support_url}';")
  else:
    wrapper.append(f"export {{ default }} from '{support_url}';")

  if settings.ENVIRONMENT_MAY_BE_NODE:
    wrapper.append(f'''
// When run as the main module under node, create the module directly.  This will
// execute any startup code along with main (if it exists).
import init from '{support_url}';
const isNode = {node_detection_code()};
if (isNode) {{
  const url = await import('node:url');
  const isMainModule = url.pathToFileURL(process.argv[1]).href === import.meta.url;
  if (isMainModule) await init();
}}''')

  write_file(wrapper_file, '\n'.join(wrapper) + '\n')

  # FIXME(sbc): This is a huge hack to rename the imports in the
  # wasm file.  Find a better way to do this.
  wasm_dis = os.path.join(building.get_binaryen_bin(), 'wasm-dis')
  mod = shared.check_call([wasm_dis, wasm_target], stdout=PIPE).stdout
  mod = mod.replace('(import "env"', f'(import "{support_url}"')
  mod = mod.replace('(import "wasi_snapshot_preview1"', f'(import "{support_url}"')

  wasm_as = os.path.join(building.get_binaryen_bin(), 'wasm-as')
  cmd = [wasm_as, '--all-features', '-o', wasm_target, '-']
  if settings.EMIT_NAME_SECTION:
    cmd.append('-g')
  shared.check_call(cmd, input=mod)


@ToolchainProfiler.profile_block('final emitting')
def phase_final_emitting(options, target, js_target, wasm_target):
  global final_js

  if shared.SKIP_SUBPROCS:
    return

  if settings.MODULARIZE and settings.MODULARIZE != 'instance':
    modularize()
  elif settings.USE_CLOSURE_COMPILER:
    module_export_name_substitution()

  # Run a final optimization pass to clean up items that were not possible to
  # optimize by Closure, or unoptimalities that were left behind by processing
  # steps that occurred after Closure.
  if settings.MINIMAL_RUNTIME == 2 and settings.USE_CLOSURE_COMPILER and settings.DEBUG_LEVEL == 0:
    args = [final_js, '-o', final_js]
    if not settings.MINIFY_WHITESPACE:
      args.append('--pretty')
    shared.run_js_tool(utils.path_from_root('tools/unsafe_optimizations.mjs'), args, cwd=utils.path_from_root('.'))
    save_intermediate('unsafe-optimizations')
    # Finally, rerun Closure compile with simple optimizations. It will be able
    # to further minify the code. (n.b. it would not be safe to run in advanced
    # mode)
    final_js = building.closure_compiler(final_js, advanced=False, extra_closure_args=settings.CLOSURE_ARGS)
    # Run unsafe_optimizations.js once more.  This allows the cleanup of newly
    # unused things that closure compiler leaves behind (e.g `new Float64Array(x)`).
    shared.run_js_tool(utils.path_from_root('tools/unsafe_optimizations.mjs'), [final_js, '-o', final_js], cwd=utils.path_from_root('.'))
    save_intermediate('unsafe-optimizations2')

  fix_js_mangling(final_js)

  # Apply pre and postjs files
  if options.extern_pre_js or options.extern_post_js:
    extern_pre_js = read_js_files(options.extern_pre_js)
    extern_post_js = read_js_files(options.extern_post_js)
    logger.debug('applying extern pre/postjses')
    src = read_file(final_js)
    final_js += '.epp.js'
    with open(final_js, 'w', encoding='utf-8') as f:
      f.write(extern_pre_js)
      f.write(src)
      f.write(extern_post_js)
    save_intermediate('extern-pre-post')

  js_manipulation.handle_license(final_js)

  # The JS is now final. Move it to its final location
  if settings.WASM_ESM_INTEGRATION:
    support_target = unsuffixed(js_target) + '.support.mjs'
    move_file(final_js, support_target)
    create_esm_wrapper(js_target, support_target, wasm_target)
    if settings.PTHREADS:
      support_target = unsuffixed(js_target) + '.pthread.mjs'
      pthread_code = building.read_and_preprocess(utils.path_from_root('src/pthread_esm_startup.mjs'), expand_macros=True)
      write_file(support_target, pthread_code)
      fix_js_mangling(support_target)
  else:
    move_file(final_js, js_target)

  target_basename = unsuffixed_basename(target)

  utils.convert_line_endings_in_file(js_target, options.output_eol)

  # If we were asked to also generate HTML, do that
  if options.oformat == OFormat.HTML:
    generate_html(target, options, js_target, target_basename,
                  wasm_target)

  if settings.SPLIT_MODULE:
    do_split_module(wasm_target, options)

  if settings.EXECUTABLE:
    make_js_executable(js_target)


@ToolchainProfiler.profile_block('binaryen')
def phase_binaryen(target, options, wasm_target):
  global final_js
  logger.debug('using binaryen')
  # whether we need to emit -g (function name debug info) in the final wasm
  debug_function_names = settings.DEBUG_LEVEL >= 2 or settings.EMIT_NAME_SECTION
  # whether we need to emit -g in the intermediate binaryen invocations (but not
  # necessarily at the very end). this is necessary if we depend on debug info
  # during compilation, even if we do not emit it at the end.
  # we track the number of causes for needing intermediate debug info so
  # that we can stop emitting it when possible - in particular, that is
  # important so that we stop emitting it before the end, and it is not in the
  # final binary (if it shouldn't be)
  intermediate_debug_info = 0
  if debug_function_names:
    intermediate_debug_info += 1
  if options.emit_symbol_map:
    intermediate_debug_info += 1
  if settings.ASYNCIFY == 1:
    intermediate_debug_info += 1

  # run wasm-opt if we have work for it: either passes, or if we are using
  # source maps (which requires some extra processing to keep the source map
  # but remove DWARF)
  passes = get_binaryen_passes(options)
  if passes:
    # if asyncify is used, we will use it in the next stage, and so if it is
    # the only reason we need intermediate debug info, we can stop keeping it
    if settings.ASYNCIFY == 1:
      intermediate_debug_info -= 1
    # currently binaryen's DWARF support will limit some optimizations; warn on
    # that. see https://github.com/emscripten-core/emscripten/issues/15269
    if settings.GENERATE_DWARF and should_run_binaryen_optimizer():
      diagnostics.warning('limited-postlink-optimizations', 'running limited binaryen optimizations because DWARF info requested (or indirectly required)')
    with ToolchainProfiler.profile_block('wasm_opt'):
      building.run_wasm_opt(wasm_target,
                            wasm_target,
                            args=passes,
                            debug=intermediate_debug_info)
      building.save_intermediate(wasm_target, 'byn.wasm')

  if settings.EVAL_CTORS:
    with ToolchainProfiler.profile_block('eval_ctors'):
      building.eval_ctors(final_js, wasm_target, debug_info=intermediate_debug_info)
      building.save_intermediate(wasm_target, 'ctors.wasm')

  # after generating the wasm, do some final operations

  if final_js:
    # >=2GB heap support requires pointers in JS to be unsigned. rather than
    # require all pointers to be unsigned by default, which increases code size
    # a little, keep them signed, and just unsign them here if we need that.
    if settings.CAN_ADDRESS_2GB:
      with ToolchainProfiler.profile_block('use_unsigned_pointers_in_js'):
        final_js = building.use_unsigned_pointers_in_js(final_js)

    if settings.USE_ASAN:
      final_js = building.instrument_js_for_asan(final_js)

    if settings.SAFE_HEAP:
      final_js = building.instrument_js_for_safe_heap(final_js)

    # shared memory growth requires some additional JS fixups.
    # note that we must do this after handling of unsigned pointers. unsigning
    # adds some >>> 0 things, while growth will replace a HEAP8 with a call to
    # a method to get the heap, and that call would not be recognized by the
    # unsigning pass.
    # we also must do this after the asan or safe_heap instrumentation, as they
    # wouldn't be able to recognize patterns produced by the growth pass.
    if settings.SHARED_MEMORY and settings.ALLOW_MEMORY_GROWTH and not settings.GROWABLE_ARRAYBUFFERS:
      with ToolchainProfiler.profile_block('apply_wasm_memory_growth'):
        final_js = building.apply_wasm_memory_growth(final_js)

    if settings.SUPPORT_BIG_ENDIAN:
      with ToolchainProfiler.profile_block('little_endian_heap'):
        final_js = building.little_endian_heap(final_js)

    if settings.OPT_LEVEL >= 2 and settings.DEBUG_LEVEL <= 2:
      # minify the JS. Do not minify whitespace if Closure is used, so that
      # Closure can print out readable error messages (Closure will then
      # minify whitespace afterwards)
      with ToolchainProfiler.profile_block('minify_wasm'):
        save_intermediate_with_wasm('preclean', wasm_target)
        final_js = building.minify_wasm_js(js_file=final_js,
                                           wasm_file=wasm_target,
                                           expensive_optimizations=will_metadce(),
                                           debug_info=intermediate_debug_info)
        save_intermediate_with_wasm('postclean', wasm_target)

    if options.use_closure_compiler:
      with ToolchainProfiler.profile_block('closure_compile'):
        final_js = building.closure_compiler(final_js, extra_closure_args=settings.CLOSURE_ARGS)
      save_intermediate('closure')

    if settings.TRANSPILE:
      with ToolchainProfiler.profile_block('transpile'):
        final_js = building.transpile(final_js)
      save_intermediate('transpile')
      # Run acorn one more time to minify whitespace after babel runs
      if settings.MINIFY_WHITESPACE:
        final_js = building.acorn_optimizer(final_js, ['--minify-whitespace'])

  symbols_file = None
  if options.emit_symbol_map:
    symbols_file = shared.replace_or_append_suffix(target, '.symbols')

  if settings.WASM2JS:
    symbols_file_js = None
    if settings.WASM == 2:
      # With normal wasm2js mode this file gets included as part of the
      # preamble, but with WASM=2 its a separate file.
      wasm2js_polyfill = building.read_and_preprocess(utils.path_from_root('src/wasm2js.js'), expand_macros=True)
      wasm2js_template = wasm_target + '.js'
      write_file(wasm2js_template, wasm2js_polyfill)
      # generate secondary file for JS symbols
      if options.emit_symbol_map:
        symbols_file_js = shared.replace_or_append_suffix(wasm2js_template, '.symbols')
    else:
      wasm2js_template = final_js
      if options.emit_symbol_map:
        symbols_file_js = shared.replace_or_append_suffix(target, '.symbols')

    wasm2js = building.wasm2js(wasm2js_template,
                               wasm_target,
                               opt_level=settings.OPT_LEVEL,
                               use_closure_compiler=options.use_closure_compiler,
                               debug_info=debug_function_names,
                               symbols_file=symbols_file,
                               symbols_file_js=symbols_file_js)

    shared.get_temp_files().note(wasm2js)

    if settings.WASM == 2:
      safe_copy(wasm2js, wasm2js_template)

    if settings.WASM != 2:
      final_js = wasm2js

    save_intermediate('wasm2js')

  generating_wasm = settings.WASM == 2 or not settings.WASM2JS

  # emit the final symbols, either in the binary or in a symbol map.
  # this will also remove debug info if we only kept it around in the intermediate invocations.
  # note that if we aren't emitting a binary (like in wasm2js) then we don't
  # have anything to do here.
  if options.emit_symbol_map:
    intermediate_debug_info -= 1
    if generating_wasm:
      building.write_symbol_map(wasm_target, symbols_file)
      if not intermediate_debug_info:
        building.strip_sections(wasm_target, wasm_target, ['name'])

  if settings.GENERATE_DWARF and settings.SEPARATE_DWARF and generating_wasm:
    # if the dwarf filename wasn't provided, use the default target + a suffix
    wasm_file_with_dwarf = settings.SEPARATE_DWARF
    if wasm_file_with_dwarf is True:
      # Historically this file has been called `.wasm.debug.wasm`
      # TODO(sbc): Should this just be `.debug.wasm`
      wasm_file_with_dwarf = get_secondary_target(target, '.wasm.debug.wasm')
    building.emit_debug_on_side(wasm_target, wasm_file_with_dwarf)

  # we have finished emitting the wasm, and so intermediate debug info will
  # definitely no longer be used tracking it.
  if debug_function_names:
    intermediate_debug_info -= 1
  assert intermediate_debug_info == 0
  # strip debug info if it was not already stripped by the last command
  if not debug_function_names and building.binaryen_kept_debug_info and generating_wasm:
    building.strip_sections(wasm_target, wasm_target, ['name'])

  # replace placeholder strings with correct subresource locations
  if final_js and settings.SINGLE_FILE and not settings.WASM2JS:
    js = read_file(final_js)

    if settings.SINGLE_FILE_BINARY_ENCODE:
      js = do_replace(js, '"<<< WASM_BINARY_DATA >>>"', binary_encode(wasm_target))
    else:
      js = do_replace(js, '<<< WASM_BINARY_DATA >>>', base64_encode(wasm_target))
    delete_file(wasm_target)
    write_file(final_js, js)


def modularize():
  global final_js
  logger.debug(f'Modularizing, creating factory function called `{settings.EXPORT_NAME}`')
  modularize_src = building.read_and_preprocess(utils.path_from_root('src/modularize.js'), expand_macros=True)
  if settings.MINIFY_WHITESPACE:
    with shared.get_temp_files().get_file(suffix='.js') as tmp:
      write_file(tmp, modularize_src)
      minified_file = building.acorn_optimizer(tmp, ['--minify-whitespace'])
      modularize_src = read_file(minified_file)

  # Replace INNER_JS_CODE in the minified code
  full_src = do_replace(modularize_src, '"<<< INNER_JS_CODE >>>"', read_file(final_js))
  final_js += '.modular.js'
  write_file(final_js, full_src)
  shared.get_temp_files().note(final_js)
  save_intermediate('modularized')

  # FIXME(https://github.com/emscripten-core/emscripten/issues/24558): Running acorn at this
  # late phase seems to cause OOM (some kind of infinite loop perhaps) in node.
  # Instead we minify src/modularize.js in isolation above.
  #if settings.MINIFY_WHITESPACE:
  #  final_js = building.acorn_optimizer(final_js, ['--minify-whitespace'])


def module_export_name_substitution():
  assert not settings.MODULARIZE
  global final_js
  logger.debug(f'Private module export name substitution with {settings.EXPORT_NAME}')
  src = read_file(final_js)
  final_js += '.module_export_name_substitution.js'
  if settings.MINIMAL_RUNTIME and not settings.ENVIRONMENT_MAY_BE_NODE and not settings.ENVIRONMENT_MAY_BE_SHELL and not settings.ENVIRONMENT_MAY_BE_AUDIO_WORKLET:
    # On the web, with MINIMAL_RUNTIME, the Module object is always provided
    # via the shell html in order to provide the .asm.js/.wasm content.
    replacement = settings.EXPORT_NAME
  else:
    replacement = "typeof %(EXPORT_NAME)s != 'undefined' ? %(EXPORT_NAME)s : {}" % {"EXPORT_NAME": settings.EXPORT_NAME}
  new_src = re.sub(r'{\s*[\'"]?__EMSCRIPTEN_PRIVATE_MODULE_EXPORT_NAME_SUBSTITUTION__[\'"]?:\s*1\s*}', replacement, src)
  assert new_src != src, 'Unable to find Closure syntax __EMSCRIPTEN_PRIVATE_MODULE_EXPORT_NAME_SUBSTITUTION__ in source!'
  write_file(final_js, new_src)
  shared.get_temp_files().note(final_js)
  save_intermediate('module_export_name_substitution')


def generate_traditional_runtime_html(target, options, js_target, wasm_target):
  script = ScriptSource()

  if settings.EXPORT_NAME != 'Module' and options.shell_html == DEFAULT_SHELL_HTML:
    # the minimal runtime shell HTML is designed to support changing the export
    # name, but the normal one does not support that currently
    exit_with_error('customizing EXPORT_NAME requires that the HTML be customized to use that name (see https://github.com/emscripten-core/emscripten/issues/10086)')

  shell = building.read_and_preprocess(options.shell_html)
  if '{{{ SCRIPT }}}' not in shell:
    exit_with_error('HTML shell must contain {{{ SCRIPT }}}, see src/shell.html for an example')

  if settings.SINGLE_FILE:
    js_contents = script.inline or ''
    js_contents += read_file(js_target)
    script.inline = read_file(js_target)
    delete_file(js_target)
  else:
    script.src = os.path.basename(js_target)
    if not settings.WASM_ASYNC_COMPILATION:
      # We need to load the wasm file before anything else, since it
      # has be synchronously ready.
      script.un_src()
      script.inline = '''
          fetch(%s).then((result) => result.arrayBuffer())
                     .then((buf) => {
                             Module.wasmBinary = buf;
                             %s;
                           });
''' % (get_subresource_location(wasm_target), script.inline)

    if settings.WASM == 2:
      # If target browser does not support WebAssembly, we need to load
      # the .wasm.js file before the main .js file.
      script.un_src()
      script.inline = '''
          function loadMainJs() {
%s
          }
          if (!window.WebAssembly || location.search.indexOf('_rwasm=0') > 0) {
            // Current browser does not support WebAssembly, load the .wasm.js JavaScript fallback
            // before the main JS runtime.
            var wasm2js = document.createElement('script');
            wasm2js.src = %s;
            wasm2js.onload = loadMainJs;
            document.body.appendChild(wasm2js);
          } else {
            // Current browser supports Wasm, proceed with loading the main JS runtime.
            loadMainJs();
          }
''' % (script.inline, get_subresource_location_js(wasm_target + '.js'))

  shell = do_replace(shell, '{{{ SCRIPT }}}', script.replacement())
  shell = shell.replace('{{{ EXPORT_NAME }}}', settings.EXPORT_NAME)
  shell = shell.replace('{{{ SHELL_CSS }}}', utils.read_file(utils.path_from_root('html/shell.css')))
  logo_filename = utils.path_from_root('media/powered_by_logo_shell.png')
  logo_b64 = base64_encode(logo_filename)
  shell = shell.replace('{{{ SHELL_LOGO }}}', f'<img id="emscripten_logo" src="data:image/png;base64,{logo_b64}">')

  check_output_file(target)
  write_file(target, shell)


@ToolchainProfiler.profile()
def minify_html(filename):
  if settings.DEBUG_LEVEL >= 2:
    return

  opts = []
  # -g1 and greater retain whitespace and comments in source
  if settings.DEBUG_LEVEL == 0:
    opts += ['--collapse-whitespace',
             '--remove-comments',
             '--remove-tag-whitespace',
             '--sort-attributes',
             '--sort-class-name']
  # -g2 and greater do not minify HTML at all
  if settings.DEBUG_LEVEL <= 1:
    opts += ['--decode-entities',
             '--collapse-boolean-attributes',
             '--remove-attribute-quotes',
             '--remove-redundant-attributes',
             '--remove-script-type-attributes',
             '--remove-style-link-type-attributes',
             '--use-short-doctype',
             '--minify-css', 'true',
             '--minify-js', 'true']

  # html-minifier also has the following options, but they look unsafe for use:
  # '--collapse-inline-tag-whitespace': removes whitespace between inline tags in visible text,
  #                                     causing words to be joined together. See
  #                                     https://github.com/terser/html-minifier-terser/issues/179
  #                                     https://github.com/emscripten-core/emscripten/issues/22188
  # '--remove-optional-tags': removes e.g. <head></head> and <body></body> tags from the page.
  #                           (Breaks at least browser.test_sdl2glshader)
  # '--remove-empty-attributes': removes all attributes with whitespace-only values.
  #                              (Breaks at least browser.test_asmfs_hello_file)
  # '--remove-empty-elements': removes all elements with empty contents.
  #                            (Breaks at least browser.test_asm_swapping)

  logger.debug(f'minifying HTML file {filename}')
  size_before = os.path.getsize(filename)
  shared.check_call(shared.get_npm_cmd('html-minifier-terser') + [filename, '-o', filename] + opts, env=shared.env_with_node_in_path())

  # HTML minifier will turn all null bytes into an escaped two-byte sequence "\0". Turn those back to single byte sequences.
  def unescape_nulls(filename):
    data = read_file(filename)
    out = []
    in_escape = False
    i = 0
    while i < len(data):
      ch = data[i]
      i += 1
      if ch == '\\':
        if in_escape:
          out.append('\\\\')
        in_escape = not in_escape
      elif in_escape:
        in_escape = False
        if ch == '0':
          out.append('\x00') # Convert '\\0' (5Ch 00h) into '\0' (00h)
        elif ch == 'x' and data[i] == '0' and data[i + 1] == '0':
          out.append('\x00') # Oddly html-minifier generates both "\\0" and "\\x00", so handle that too.
          i += 2
        else:
          out.append('\\')
          out.append(ch)
      else:
        out.append(ch)

    write_file(filename, ''.join(out))

  unescape_nulls(filename)

  size_after = os.path.getsize(filename)
  delta = size_after - size_before
  logger.debug(f'HTML minification shrunk {filename} from {size_before} to {size_after} bytes, delta={delta} ({delta * 100.0 / size_before:+.2f}%)')


def generate_html(target, options, js_target, target_basename, wasm_target):
  logger.debug('generating HTML')

  if settings.MINIMAL_RUNTIME:
    generate_minimal_runtime_html(target, options, js_target, target_basename)
  else:
    generate_traditional_runtime_html(target, options, js_target, wasm_target)

  if settings.MINIFY_HTML and (settings.OPT_LEVEL >= 1 or settings.SHRINK_LEVEL >= 1):
    minify_html(target)

  utils.convert_line_endings_in_file(target, options.output_eol)


def find_library(lib, lib_dirs):
  for lib_dir in lib_dirs:
    path = os.path.join(lib_dir, lib)
    if os.path.isfile(path):
      logger.debug('found library "%s" at %s', lib, path)
      return path
  return None


def map_to_js_libs(library_name):
  """Given the name of a special Emscripten-implemented system library, returns an
  pair containing
  1. Array of absolute paths to JS library files, inside emscripten/src/ that corresponds to the
     library name. `None` means there is no mapping and the library will be processed by the linker
     as a require for normal native library.
  2. Optional name of a corresponding native library to link in.
  """
  # Some native libraries are implemented in Emscripten as system side JS libraries
  library_map = {
    'embind': ['libembind.js', 'libemval.js'],
    'EGL': ['libegl.js'],
    'GL': ['libwebgl.js', 'libhtml5_webgl.js'],
    'webgl.js': ['libwebgl.js', 'libhtml5_webgl.js'],
    'GLESv2': ['libwebgl.js'],
    # N.b. there is no GLESv3 to link to (note [f] in https://www.khronos.org/registry/implementers_guide.html)
    'GLEW': ['libglew.js'],
    'glfw': ['libglfw.js'],
    'glfw3': ['libglfw.js'],
    'GLU': [],
    'glut': ['libglut.js'],
    'openal': ['libopenal.js'],
    'X11': ['libxlib.js'],
    'SDL': ['libsdl.js'],
    'uuid': ['libuuid.js'],
    'fetch': ['libfetch.js'],
    'websocket': ['libwebsocket.js'],
    # These 4 libraries are separate under glibc but are all rolled into
    # libc with musl.  For compatibility with glibc we just ignore them
    # completely.
    'dl': [],
    'm': [],
    'rt': [],
    'pthread': [],
    # This is the name of GNU's C++ standard library. We ignore it here
    # for compatibility with GNU toolchains.
    'stdc++': [],
    'SDL2_mixer': [],
  }

  if library_name in library_map:
    libs = library_map[library_name]
    logger.debug('Mapping library `%s` to JS libraries: %s' % (library_name, libs))
    return libs

  return None


def process_libraries(options, flags):
  """Process `-l` and `--js-library` flags."""
  new_flags = []
  system_libs_map = system_libs.Library.get_usable_variations()

  for flag in flags:
    if flag.startswith('--js-library='):
      js_lib = flag.split('=', 1)[1]
      settings.JS_LIBRARIES.append(js_lib)
      continue
    if not flag.startswith('-l'):
      new_flags.append(flag)
      continue
    lib = flag.removeprefix('-l')

    logger.debug('looking for library "%s"', lib)

    js_libs = map_to_js_libs(lib)
    if js_libs is not None:
      for l in js_libs:
        add_system_js_lib(l)

    # We don't need to resolve system libraries to absolute paths here, we can just
    # let wasm-ld handle that.  However, we do want to map to the correct variant.
    # For example we map `-lc` to `-lc-mt` if we are building with threading support.
    if 'lib' + lib in system_libs_map:
      lib = system_libs_map['lib' + lib].get_link_flag()
      new_flags.append(lib)
      continue

    if js_libs is not None:
      continue

    if lib.endswith('.js'):
      name = 'lib' + lib
      path = find_library(name, options.lib_dirs)
      if not path:
        exit_with_error(f'unable to find library {flag}')
      settings.JS_LIBRARIES.append(os.path.abspath(path))
      continue

    static_lib = f'lib{lib}.a'
    if not settings.RELOCATABLE and not settings.MAIN_MODULE and not find_library(static_lib, options.lib_dirs):
      # Normally we can rely on the native linker to expand `-l` args.
      # However, emscripten also supports fake `.so` files that are actually
      # just regular object files.  This means we need to support `.so` files even
      # when statically linking.  The native linker (wasm-ld) will otherwise
      # ignore .so files in this mode.
      found_dylib = False
      for ext in DYLIB_EXTENSIONS:
        name = 'lib' + lib + ext
        path = find_library(name, options.lib_dirs)
        if path and not building.is_wasm_dylib(path):
          found_dylib = True
          new_flags.append(path)
          break

      if found_dylib:
        continue

    new_flags.append(flag)

  return new_flags


def apply_library_settings(linker_args):
  for arg in linker_args:
    if not arg.startswith('-l'):
      continue
    library_name = arg[2:]
    settings_map = {
      'embind': {'EMBIND': 1},
      'glfw': {'USE_GLFW': 2},
      'glfw3': {'USE_GLFW': 3},
      'SDL': {'USE_SDL': 1},
      'SDL2_mixer': {'USE_SDL_MIXER': 2},
    }

    if library_name in settings_map:
      for key, value in settings_map[library_name].items():
        default_setting(key, value)


class ScriptSource:
  def __init__(self):
    self.src = None # if set, we have a script to load with a src attribute
    self.inline = None # if set, we have the contents of a script to write inline in a script

  def un_src(self):
    """Use this if you want to modify the script and need it to be inline."""
    if self.src is None:
      return
    quoted_src = quote(self.src)
    if settings.EXPORT_ES6:
      self.inline = f'''
        import("./{quoted_src}").then(exports => exports.default(Module))
      '''
    else:
      self.inline = f'''
            var script = document.createElement('script');
            script.src = "{quoted_src}";
            document.body.appendChild(script);
      '''
    self.src = None

  def replacement(self):
    """Returns the script tag to replace the {{{ SCRIPT }}} tag in the target"""
    assert (self.src or self.inline) and not (self.src and self.inline)
    if self.src:
      src = quote(self.src)
      filename = f'./{src}'
      if settings.EXPORT_ES6:
        return f'''
        <script type="module">
          import initModule from "{filename}";
          initModule(Module);
        </script>
        '''
      else:
        if settings.MODULARIZE:
          return f'<script type="text/javascript" src="{src}"></script>'
        else:
          return f'<script async type="text/javascript" src="{src}"></script>'
    else:
      return f'<script id="mainScript">\n{self.inline}\n</script>'


def filter_out_fake_dynamic_libs(options, inputs):
  """Filter out "fake" dynamic libraries that are really just intermediate object files."""
  def is_fake_dylib(input_file):
    if get_file_suffix(input_file) in DYLIB_EXTENSIONS and os.path.exists(input_file) and not building.is_wasm_dylib(input_file):
      if not options.ignore_dynamic_linking:
        diagnostics.warning('emcc', 'ignoring dynamic library %s when generating an object file, this will need to be included explicitly in the final link', os.path.basename(input_file))
      return True
    else:
      return False

  return [f for f in inputs if not is_fake_dylib(f)]


def filter_out_duplicate_fake_dynamic_libs(inputs):
  """Filter out duplicate "fake" shared libraries (intermediate object files).

  See test_core.py:test_redundant_link
  """
  seen = set()

  def check(input_file):
    if get_file_suffix(input_file) in DYLIB_EXTENSIONS and not building.is_wasm_dylib(input_file):
      abspath = os.path.abspath(input_file)
      if abspath in seen:
        return False
      seen.add(abspath)
    return True

  return [f for f in inputs if check(f)]


def process_dynamic_libs(dylibs, lib_dirs):
  extras = []
  seen = set()
  to_process = dylibs.copy()
  while to_process:
    dylib = to_process.pop()
    dylink = webassembly.parse_dylink_section(dylib)
    for needed in dylink.needed:
      if needed in seen:
        continue
      path = find_library(needed, lib_dirs)
      if path:
        extras.append(path)
        seen.add(needed)
      else:
        exit_with_error(f'{os.path.normpath(dylib)}: shared library dependency not found in library path: `{needed}`. (library path: {lib_dirs}')
      to_process.append(path)

  dylibs += extras
  for dylib in dylibs:
    exports = webassembly.get_exports(dylib)
    exports = {e.name for e in exports}
    # EM_JS function are exports with a special prefix.  We need to strip
    # this prefix to get the actual symbol name.  For the main module, this
    # is handled by extract_metadata.py.
    exports = [e.removeprefix('__em_js__') for e in exports]
    settings.SIDE_MODULE_EXPORTS.extend(sorted(exports))

    imports = webassembly.get_imports(dylib)
    imports = [i.field for i in imports if i.kind in (webassembly.ExternType.FUNC, webassembly.ExternType.GLOBAL, webassembly.ExternType.TAG)]
    # For now we ignore `invoke_` functions imported by side modules and rely
    # on the dynamic linker to create them on the fly.
    # TODO(sbc): Integrate with metadata.invoke_funcs that comes from the
    # main module to avoid creating new invoke functions at runtime.
    imports = set(imports)
    imports = {i for i in imports if not i.startswith('invoke_')}
    weak_imports = webassembly.get_weak_imports(dylib)
    strong_imports = sorted(imports.difference(weak_imports))
    logger.debug('Adding symbols requirements from `%s`: %s', dylib, imports)

    mangled_imports = [shared.asmjs_mangle(e) for e in sorted(imports)]
    mangled_strong_imports = [shared.asmjs_mangle(e) for e in strong_imports]
    for sym in weak_imports:
      mangled = shared.asmjs_mangle(sym)
      if mangled not in settings.SIDE_MODULE_IMPORTS and mangled not in building.user_requested_exports:
        settings.WEAK_IMPORTS.append(sym)
    settings.SIDE_MODULE_IMPORTS.extend(mangled_imports)
    settings.EXPORT_IF_DEFINED.extend(sorted(imports))
    settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.extend(sorted(imports))
    building.user_requested_exports.update(mangled_strong_imports)


def unmangle_symbols_from_cmdline(symbols):
  def unmangle(x):
    return x.replace('.', ' ').replace('#', '&').replace('?', ',')

  if type(symbols) is list:
    return [unmangle(x) for x in symbols]
  return unmangle(symbols)


def get_secondary_target(target, ext):
  # Depending on the output format emscripten creates zero or more secondary
  # output files (e.g. the .wasm file when creating JS output, or the
  # .js and the .wasm file when creating html output.
  # This function names the secondary output files, while ensuring they
  # never collide with the primary one.
  base = unsuffixed(target)
  if get_file_suffix(target) == ext:
    base += '_'
  return base + ext


def dedup_list(lst):
  # Since we require python 3.6, that ordering of dictionaries is guaranteed
  # to be insertion order so we can use 'dict' here but not 'set'.
  return list(dict.fromkeys(lst))


def check_output_file(f):
  if os.path.isdir(f):
    exit_with_error(f'cannot write output file `{f}`: Is a directory')


def move_file(src, dst):
  logging.debug('move: %s -> %s', src, dst)
  check_output_file(dst)
  src = os.path.abspath(src)
  dst = os.path.abspath(dst)
  if src == dst:
    return
  if dst == os.devnull:
    return
  shutil.move(src, dst)


def binary_encode(filename):
  """This function encodes the given binary byte array to a UTF-8 string, by
  encoding each byte values as UTF-8, except for specific byte values that
  are escaped as two bytes. This kind of encoding results in a string that will
  compress well by both gzip and brotli, unlike base64 encoding binary data
  would do.
  """

  data = utils.read_binary(filename)

  # Decide whether to enclose the generated binary data in single-quotes '' or
  # double-quotes "" by looking at which character ends up requiring fewer
  # escapes of that string character.
  num_single_quotes = data.count(ord("'"))
  num_double_quotes = data.count(ord('"'))
  quote_char = ord("'") if num_single_quotes < num_double_quotes else ord('"')

  out = bytearray(len(data) * 2 + 2) # Size output buffer conservatively
  out[0] = quote_char # Emit string start quote
  i = 1
  for d in data:
    if d == quote_char:
      buf = [ord('\\'), d] # Escape the string quote character with a backslash since we are writing the binary data inside a string.
    elif d == ord('\r'):
      buf = [ord('\\'), ord('r')] # Escape carriage return 0x0D as \r -> 2 bytes
    elif d == ord('\n'):
      buf = [ord('\\'), ord('n')] # Escape newline 0x0A as \n -> 2 bytes
    elif d == ord('\\'):
      buf = [ord('\\'), ord('\\')] # Escape backslash \ as \\ -> 2 bytes
    else:
      buf = chr(d).encode('utf-8') # Otherwise write the original value encoded in UTF-8 (1 or 2 bytes).
    for b in buf: # Write the bytes to output buffer
      out[i] = b
      i += 1
  out[i] = quote_char # Emit string end quote
  i += 1
  return out[0:i].decode('utf-8') # Crop output buffer to the actual used size


# Returns the subresource location for run-time access
def get_subresource_location(path, mimetype='application/octet-stream'):
  if settings.SINGLE_FILE:
    if settings.SINGLE_FILE_BINARY_ENCODE:
      return binary_encode(path)
    return f'"data:{mimetype};base64,{base64_encode(path)}"'
  else:
    return f'"{os.path.basename(path)}"'


def get_subresource_location_js(path):
  return get_subresource_location(path, 'text/javascript')


@ToolchainProfiler.profile()
def package_files(options, target):
  rtn = []
  logger.debug('setting up files')
  file_args = ['--from-emcc']
  if options.preload_files:
    file_args.append('--preload')
    file_args += options.preload_files
  if options.embed_files:
    file_args.append('--embed')
    file_args += options.embed_files
  if options.exclude_files:
    file_args.append('--exclude')
    file_args += options.exclude_files
  if options.use_preload_cache:
    file_args.append('--use-preload-cache')
  if settings.LZ4:
    file_args.append('--lz4')
  if options.use_preload_plugins:
    file_args.append('--use-preload-plugins')
  if not settings.ENVIRONMENT_MAY_BE_NODE:
    file_args.append('--no-node')
  if options.embed_files:
    if settings.MEMORY64:
      file_args += ['--wasm64']
    object_file = in_temp('embedded_files.o')
    file_args += ['--obj-output=' + object_file]
    rtn.append(object_file)

  cmd = building.get_command_with_possible_response_file(
    [shared.FILE_PACKAGER, utils.replace_suffix(target, '.data')] + file_args)
  if options.preload_files:
    # Preloading files uses --pre-js code that runs before the module is loaded.
    file_code = shared.check_call(cmd, stdout=PIPE).stdout
    js_manipulation.add_files_pre_js(settings.PRE_JS_FILES, file_code)
  else:
    # Otherwise, we are embedding files, which does not require --pre-js code,
    # and instead relies on a static constructor to populate the filesystem.
    shared.check_call(cmd)

  return rtn


@ToolchainProfiler.profile_block('calculate linker inputs')
def phase_calculate_linker_inputs(options, linker_args):
  using_lld = not (options.oformat == OFormat.OBJECT and settings.LTO)

  linker_args = filter_link_flags(linker_args, using_lld)

  # If we are linking to an intermediate object then ignore other
  # "fake" dynamic libraries, since otherwise we will end up with
  # multiple copies in the final executable.
  if options.oformat == OFormat.OBJECT or options.ignore_dynamic_linking:
    linker_args = filter_out_fake_dynamic_libs(options, linker_args)
  else:
    linker_args = filter_out_duplicate_fake_dynamic_libs(linker_args)

  if settings.MAIN_MODULE:
    process_dynamic_libs(options.dylibs, options.lib_dirs)

  return linker_args


def calc_extra_ldflags(options):
  extra_args = []
  system_libpath = str(cache.get_lib_dir(absolute=True))
  system_js_path = utils.path_from_root('src', 'lib')
  options.lib_dirs.append(system_libpath)
  options.lib_dirs.append(system_js_path)
  extra_args.append('-L' + system_libpath)
  extra_args.append('-L' + system_js_path)

  if settings.FETCH:
    extra_args.append('-lfetch')
  if settings.STB_IMAGE:
    extra_args.append('-lstb_image')
  if settings.WASMFS and settings.NODERAWFS:
    # wasmfs will be included normally in system_libs.py, but we must include
    # noderawfs in a forced manner so that it is always linked in (the hook it
    # implements can remain unimplemented, so it won't be linked in
    # automatically)
    # TODO: find a better way to do this
    extra_args.append('--whole-archive')
    extra_args.append('-lwasmfs_noderawfs')
    extra_args.append('--no-whole-archive')

  return extra_args


def run_post_link(wasm_input, options, linker_args):
  settings.limit_settings(None)
  target, wasm_target = phase_linker_setup(options, linker_args)
  process_libraries(options, linker_args)
  phase_post_link(options, wasm_input, wasm_target, target, {})


def run(options, linker_args):
  # We have now passed the compile phase, allow reading/writing of all settings.
  settings.limit_settings(None)

  if settings.RUNTIME_LINKED_LIBS:
    linker_args += settings.RUNTIME_LINKED_LIBS

  if not linker_args:
    exit_with_error('no input files')

  if options.output_file and options.output_file.startswith('-'):
    exit_with_error(f'invalid output filename: `{options.output_file}`')

  target, wasm_target = phase_linker_setup(options, linker_args)

  linker_args = process_libraries(options, linker_args)

  # Link object files using wasm-ld or llvm-link (for bitcode linking)
  linker_args = phase_calculate_linker_inputs(options, linker_args)

  # Embed and preload files
  if len(options.preload_files) or len(options.embed_files):
    linker_args += package_files(options, target)

  if options.oformat == OFormat.OBJECT:
    logger.debug(f'link_to_object: {linker_args} -> {target}')
    building.link_to_object(linker_args, target)
    logger.debug('stopping after linking to object file')
    return 0

  system_libs = phase_calculate_system_libraries(options)
  # Only add system libraries that have not already been specified.
  # This avoids issues where the user explicitly includes, for example, `-lGL`.
  # This is not normally a problem except in the case of -sMAIN_MODULE=1 where
  # the duplicate library would result in duplicate symbols.
  for s in system_libs:
    if s.startswith('-l') and s in linker_args:
      continue
    linker_args.append(s)

  js_syms = {}
  if (not settings.SIDE_MODULE or settings.ASYNCIFY) and not shared.SKIP_SUBPROCS:
    js_info = get_js_sym_info()
    if not settings.SIDE_MODULE:
      js_syms = js_info['deps']
      if settings.LINKABLE:
        for native_deps in js_syms.values():
          settings.REQUIRED_EXPORTS += native_deps
      else:
        def add_js_deps(sym):
          if sym in js_syms:
            native_deps = js_syms[sym]
            if native_deps:
              settings.REQUIRED_EXPORTS += native_deps

        for sym in settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE:
          add_js_deps(sym)
        for sym in js_info['extraLibraryFuncs']:
          add_js_deps(sym)
        for sym in settings.EXPORTED_RUNTIME_METHODS:
          add_js_deps(shared.demangle_c_symbol_name(sym))
        for sym in settings.EXPORTED_FUNCTIONS:
          add_js_deps(shared.demangle_c_symbol_name(sym))
    if settings.ASYNCIFY:
      settings.ASYNCIFY_IMPORTS_EXCEPT_JS_LIBS = settings.ASYNCIFY_IMPORTS[:]
      settings.ASYNCIFY_IMPORTS += ['*.' + x for x in js_info['asyncFuncs']]

  base_metadata = phase_link(linker_args, wasm_target, js_syms)

  # Special handling for when the user passed '-Wl,--version'.  In this case the linker
  # does not create the output file, but just prints its version and exits with 0.
  if '--version' in linker_args:
    return 0

  # TODO(sbc): In theory we should really run the whole pipeline even if the output is
  # /dev/null, but that will take some refactoring
  if target == os.devnull:
    return 0

  # Perform post-link steps (unless we are running bare mode)
  if options.oformat != OFormat.BARE:
    phase_post_link(options, wasm_target, wasm_target, target, js_syms, base_metadata)

  return 0
