current commit
261030.12 · Enable `NODE_COMPILE_CACHE` when running JS compiler tools (#28116)
# Copyright 2020 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 importlib
import json
import logging
import os
import re
import shlex
import shutil
import subprocess
import sys
from subprocess import PIPE

from . import (
  cache,
  cmdline,
  colored_logger,
  config,
  diagnostics,
  feature_matrix,
  js_optimizer,
  ports,
  response_file,
  shared,
  utils,
  webassembly,
)
from .settings import settings, user_settings
from .shared import (
  CLANG_CC,
  CLANG_CXX,
  DEBUG,
  EMAR,
  EMCC,
  EMRANLIB,
  EMXX,
  asmjs_mangle,
  check_call,
  demangle_c_symbol_name,
  exit_with_error,
  get_emscripten_temp_dir,
  is_c_symbol,
  path_from_root,
)
from .toolchain_profiler import ToolchainProfiler
from .utils import WINDOWS, run_process

logger = logging.getLogger('building')

#  Building
binaryen_checked = False
EXPECTED_BINARYEN_VERSION = 133
WASM_LD = shared.llvm_tool_path('wasm-ld')
LLVM_DWARFDUMP = shared.llvm_tool_path('llvm-dwarfdump')
LLVM_OBJCOPY = shared.llvm_tool_path('llvm-objcopy')
LLVM_OBJDUMP = shared.llvm_tool_path('llvm-objdump')
LLVM_NM = shared.llvm_tool_path('llvm-nm')

# the exports the user requested
user_requested_exports: set[str] = set()
# JS library symbols exported via the `__export` decorator.
extra_js_exports: set[str] = set()
# Mangled wasm exports wasm-bindgen's glue reaches by name, kept off the public surface.
wasm_bindgen_internal_exports: set[str] = set()
# A list of feature flags to pass to each binaryen invocation (like `wasm-opt`,
# etc.). This is received by the first call to binaryen (e.g. `wasm-emscripten-finalize`)
# which reads it using `--detect-features`.
binaryen_features: list[str] = []


def get_building_env():
  cache.ensure()
  env = os.environ.copy()
  # point CC etc. to the em* tools.
  env['CC'] = EMCC
  env['CXX'] = EMXX
  env['AR'] = EMAR
  env['LD'] = EMCC
  env['NM'] = LLVM_NM
  env['LDSHARED'] = f'{EMCC} -shared'
  env['RANLIB'] = EMRANLIB
  env['EMSCRIPTEN_TOOLS'] = path_from_root('tools')
  env['HOST_CC'] = CLANG_CC
  env['HOST_CXX'] = CLANG_CXX
  env['HOST_CFLAGS'] = '-W' # if set to nothing, CFLAGS is used, which we don't want
  env['HOST_CXXFLAGS'] = '-W' # if set to nothing, CXXFLAGS is used, which we don't want
  env['PKG_CONFIG_LIBDIR'] = cache.get_sysroot_dir('local/lib/pkgconfig') + os.path.pathsep + cache.get_sysroot_dir('lib/pkgconfig')
  env['PKG_CONFIG_PATH'] = os.environ.get('EM_PKG_CONFIG_PATH', '')
  env['EMSCRIPTEN'] = path_from_root()
  env['PATH'] = cache.get_sysroot_dir('bin') + os.pathsep + env['PATH']
  env['ACLOCAL_PATH'] = cache.get_sysroot_dir('share/aclocal')
  env['CROSS_COMPILE'] = path_from_root('em') # produces /path/to/emscripten/em , which then can have 'cc', 'ar', etc appended to it
  return env


def llvm_backend_args():
  # disable slow and relatively unimportant optimization passes
  args = ['-combiner-global-alias-analysis=false']

  # asm.js-style exception handling
  if settings.EXCEPTION_CATCHING_ALLOWED:
    # When 'main' has a non-standard signature, LLVM outlines its content out to
    # '__original_main'. So we add it to the allowed list as well.
    if 'main' in settings.EXCEPTION_CATCHING_ALLOWED:
      settings.EXCEPTION_CATCHING_ALLOWED += ['__original_main', '__main_argc_argv']
    allowed = ','.join(settings.EXCEPTION_CATCHING_ALLOWED)
    args += ['-emscripten-cxx-exceptions-allowed=' + allowed]

  match settings.SUPPORT_LONGJMP:
    case 'emscripten':
      # asm.js-style setjmp/longjmp handling
      args += ['-enable-emscripten-sjlj']
    case 'wasm':
      # setjmp/longjmp handling using Wasm EH
      args += ['-wasm-enable-sjlj']

  if settings.WASM_EXCEPTIONS or settings.SUPPORT_LONGJMP == 'wasm':
    if settings.WASM_LEGACY_EXCEPTIONS:
      args += ['-wasm-use-legacy-eh']
    else:
      args += ['-wasm-use-legacy-eh=0']

  # better (smaller, sometimes faster) codegen, see binaryen#1054
  # and https://bugs.llvm.org/show_bug.cgi?id=39488
  args += ['-disable-lsr']

  return args


@ToolchainProfiler.profile()
def link_to_object(args, target):
  link_lld([*args, '--relocatable'], target)


def side_module_external_deps(external_symbols):
  """Find the list of the external symbols that are needed by the linked side modules."""
  deps = set()
  for sym in settings.SIDE_MODULE_IMPORTS:
    sym = demangle_c_symbol_name(sym)
    if sym in external_symbols:
      deps = deps.union(external_symbols[sym])
  return sorted(deps)


def create_stub_object(external_symbols):
  """Create a stub object, based on the JS library symbols and their dependencies, for use by wasm-ld."""
  stubfile = shared.get_temp_files().get('libemscripten_js_symbols.so').name
  stubs = ['#STUB']
  for name, deps in external_symbols.items():
    if not name.startswith('$'):
      stubs.append(f"{name}: {','.join(deps)}")
  utils.write_file(stubfile, '\n'.join(stubs))
  return stubfile


def lld_flags_for_executable(external_symbols):
  cmd = []
  if external_symbols:
    if settings.INCLUDE_FULL_LIBRARY:
      # When INCLUDE_FULL_LIBRARY is set try to export every possible
      # native dependency of a JS function.
      all_deps = set()
      for deps in external_symbols.values():
        for dep in deps:
          if dep not in all_deps:
            cmd.append('--export-if-defined=' + dep)
          all_deps.add(dep)
    stub = create_stub_object(external_symbols)
    cmd.append(stub)

  if settings.FAKE_DYLIBS or (not settings.MAIN_MODULE and not settings.SIDE_MODULE):
    cmd.append('-Bstatic')
  else:
    # wasm-ld still defaults to static linking by default. If that ever changes, we can remove this line.
    cmd.append('-Bdynamic')

  if not settings.ERROR_ON_UNDEFINED_SYMBOLS:
    cmd.append('--import-undefined')

  if settings.IMPORTED_MEMORY:
    cmd.append('--import-memory')

  if settings.SHARED_MEMORY:
    cmd.append('--shared-memory')

  # wasm-ld can strip debug info for us. this strips both the Names
  # section and DWARF, so we can only use it when we don't need any of
  # those things.
  if (not settings.GENERATE_DWARF and
      not settings.EMIT_SYMBOL_MAP and
      not settings.GENERATE_SOURCE_MAP and
      not settings.EMIT_NAME_SECTION and
      not settings.ASYNCIFY):
    cmd.append('--strip-debug')

  if cmdline.options.lto:
    if not settings.EXIT_RUNTIME:
      # The WebAssembly backend can generate new references to `__cxa_atexit` at
      # LTO time.  This `-u` flag forces the `__cxa_atexit` symbol to be
      # included at LTO time.  For other such symbols we exclude them from LTO
      # and always build them as normal object files, but that would inhibit the
      # LowerGlobalDtors optimization which allows destructors to be completely
      # removed when __cxa_atexit is a no-op.
      cmd.append('-u__cxa_atexit')
    if not settings.DISABLE_EXCEPTION_CATCHING:
      # The WebAssembly backend can generate new references to `__cxa_find_matching_catch_N`
      # at LTO time, which depends on `__cxa_can_catch` in libc++abi.
      cmd.append('-u__cxa_can_catch')

  c_exports = [e for e in settings.EXPORTED_FUNCTIONS if is_c_symbol(e)]
  # Strip the leading underscores
  c_exports = [demangle_c_symbol_name(e) for e in c_exports]
  # Filter out symbols external/JS symbols
  c_exports = [e for e in c_exports if e not in external_symbols]
  c_exports += settings.REQUIRED_EXPORTS
  if settings.MAIN_MODULE:
    c_exports += side_module_external_deps(external_symbols)
  for export in c_exports:
    if settings.ERROR_ON_UNDEFINED_SYMBOLS:
      cmd.append(f'--export={export}')
    else:
      cmd.append(f'--export-if-defined={export}')

  cmd.extend(f'--export-if-defined={e}' for e in settings.EXPORT_IF_DEFINED)

  if settings.MAIN_MODULE or settings.SIDE_MODULE:
    cmd.append('--unresolved-symbols=import-dynamic')
    if not settings.WASM_BIGINT:
      # When we don't have WASM_BIGINT available, JS signature legalization
      # in binaryen will mutate the signatures of the imports/exports of our
      # shared libraries.  Because of this we need to disable signature
      # checking of shared library functions in this case.
      cmd.append('--no-shlib-sigcheck')

  if settings.SIDE_MODULE:
    cmd.append('-shared')
    # Unlike `-shared`, `-sSIDE_MODULE=2` disables `--export-dynamic` so that
    # unexported symbols are DCE'd.  If both `-shared` and `-sSIDE_MODULE` are
    # specified, `-sSIDE_MODULE` takes precedence.
    if not settings.LINKABLE and (not cmdline.options.shared or 'SIDE_MODULE' in user_settings):
      cmd.append('--no-export-dynamic')
  else:
    if settings.LINKABLE:
      cmd.append('--export-dynamic')
    cmd.append('--export-table')
    if settings.ALLOW_TABLE_GROWTH:
      cmd.append('--growable-table')

  if not settings.SIDE_MODULE:
    cmd += ['-z', f'stack-size={settings.STACK_SIZE}']

    if settings.ALLOW_MEMORY_GROWTH or (settings.IMPORTED_MEMORY and settings.MAXIMUM_MEMORY > settings.INITIAL_MEMORY):
      cmd += [f'--max-memory={settings.MAXIMUM_MEMORY}']
    else:
      cmd += ['--no-growable-memory']

    if settings.INITIAL_HEAP != -1:
      cmd += [f'--initial-heap={settings.INITIAL_HEAP}']
    if settings.INITIAL_MEMORY != -1:
      cmd += [f'--initial-memory={settings.INITIAL_MEMORY}']

    if settings.STANDALONE_WASM:
      # when settings.EXPECT_MAIN is set we fall back to wasm-ld default of _start
      if not settings.EXPECT_MAIN:
        cmd += ['--entry=_initialize']
    elif settings.PROXY_TO_PTHREAD:
      cmd += ['--entry=_emscripten_proxy_main']
    else:
      # TODO(sbc): Avoid passing --no-entry when we know we have an entry point.
      # For now we need to do this since the entry point can be either `main` or
      # `__main_argv_argc`, but we should address that by using a single `_start`
      # function like we do in STANDALONE_WASM mode.
      cmd += ['--no-entry']

  # The default for `--stack-first` is transitioning from disabled to
  # enabled.  So be explicit in all cases for now.
  if settings.STACK_FIRST:
    cmd.append('--stack-first')
  else:
    cmd.append('--no-stack-first')

  if not settings.SIDE_MODULE:
    cmd.append(f'--table-base={settings.TABLE_BASE}')
    if not settings.STACK_FIRST:
      cmd.append(f'--global-base={settings.GLOBAL_BASE}')

  if feature_matrix.caniuse(feature_matrix.Feature.EXTENDED_CONST):
    cmd.append('--extra-features=extended-const')

  return cmd


def get_wasm_bindgen_exported_symbols(input_files):
  nm_args = [LLVM_NM, '--defined-only', '--extern-only', '--format=just-symbols',
             '--print-file-name', '--quiet']
  nm_args += input_files

  result = check_call(nm_args, stdout=PIPE)
  symbols = []
  for line in result.stdout.splitlines():
    _, symbol = line.split()
    # Skip mangled (non-C) symbols
    if symbol.startswith(('_Z', '_R', 'anon.')):
      continue
    symbols.append(symbol)

  return symbols


def lld_flags(args):
  # lld doesn't currently support --start-group/--end-group since the
  # semantics are more like the windows linker where there is no need for
  # grouping.
  args = [a for a in args if a not in {'--start-group', '--end-group'}]

  # Emscripten currently expects linkable output (SIDE_MODULE/MAIN_MODULE) to
  # include all archive contents.
  if settings.LINKABLE:
    args.insert(0, '--whole-archive')
    args.append('--no-whole-archive')

  if settings.STRICT and '--no-fatal-warnings' not in args:
    args.append('--fatal-warnings')

  if any(a in args for a in ('--strip-all', '-s')):
    # Tell wasm-ld to always generate a target_features section even if --strip-all/-s
    # is passed.
    args.append('--keep-section=target_features')

  if settings.MEMORY64:
    args.append('-mwasm64')

  for a in llvm_backend_args():
    args += ['-mllvm', a]

  return args


def link_lld(args, target, external_symbols=None):
  # runs lld to link things.
  if not os.path.exists(WASM_LD):
    exit_with_error('linker binary not found in LLVM directory: %s', WASM_LD)
  cmd = [WASM_LD, '-o', target]
  # For relocatable output (generating an object file) we don't pass any of the
  # normal linker flags that are used when building and executable
  if '--relocatable' not in args and '-r' not in args:
    cmd += lld_flags_for_executable(external_symbols)
  cmd += lld_flags(args)
  cmd = get_command_with_possible_response_file(cmd)
  if settings.LINK_AS_CXX:
    check_call(cmd)
  else:
    # When not running C++ mode we currently capture the stderr of the linker
    # so that we can recommend using `em++` when there are libc++ symbols missing.
    # TODO: Remove this extra complexity one day.
    if colored_logger.ansi_color_available():
      # We force color diagnostics from wasm-ld when we know that they are available
      # in the current TTY.  Without this, the use of stderr=PIPE would cause
      # wasm-ld to always disable color output.
      cmd.append('--color-diagnostics=always')
    try:
      proc = shared.run_process(cmd, stderr=subprocess.PIPE)
      if proc.stderr:
        sys.stderr.write(proc.stderr)
    except subprocess.CalledProcessError as e:
      sys.stderr.write(e.stderr)
      cxx_symbols = ('std::', 'operator new', 'operator delete', 'vtable for', 'typeinfo for', '__cxa_')
      if any(sym in e.stderr for sym in cxx_symbols):
        diagnostics.warn("link failed with undefined C++ symbols. Try linking with 'em++' or passing '-sDEFAULT_TO_CXX'")
      exit_with_error("'%s' failed (%s)", shlex.join(cmd), shared.returncode_to_str(e.returncode))


def get_command_with_possible_response_file(cmd):
  # One of None, 0 or 1. (None: do default decision, 0: force disable, 1: force enable)
  force_response_files = os.getenv('EM_FORCE_RESPONSE_FILES')

  # Different OS have different limits. The most limiting usually is Windows one
  # which is set at 8191 characters. We could just use that, but it leads to
  # problems when invoking wrappers (e.g. pylauncher.exe), which, in turn,
  # pass arguments to some longer command like `(full path to Clang) ...args`.
  # In that scenario, even if the initial command line is short enough, the
  # subprocess can still run into the Command Line Too Long error.
  # Reduce the limit by ~1K for now to be on the safe side, but we might need to
  # adjust this in the future if it turns out not to be enough.
  if (len(shlex.join(cmd)) <= 7000 and force_response_files != '1') or force_response_files == '0':
    return cmd

  logger.debug(f'using response file for {cmd[0]}')
  filename = response_file.create_response_file(cmd[1:], shared.TEMP_DIR)
  new_cmd = [cmd[0], "@" + filename]
  return new_cmd


def opt_level_to_str(opt_level, shrink_level=0):
  # convert opt_level/shrink_level pair to a string argument like -O1
  if opt_level == 0:
    return '-O0'
  if shrink_level == 1:
    return '-Os'
  elif shrink_level >= 2:
    return '-Oz'
  else:
    return f'-O{min(opt_level, 3)}'


def run_js_optimizer(filename, passes):
  try:
    return js_optimizer.run_on_file(filename, passes)
  except subprocess.CalledProcessError as e:
    exit_with_error("'%s' failed (%d)", ' '.join(e.cmd), e.returncode)


def acorn_optimizer(filename, passes, extra_info=None, return_output=False, worker_js=False):
  optimizer = path_from_root('tools/acorn-optimizer.mjs')
  original_filename = filename
  if extra_info is not None and not shared.SKIP_SUBPROCS:
    temp_files = shared.get_temp_files()
    temp = temp_files.get('.js', prefix='emcc_acorn_info_').name
    shutil.copyfile(filename, temp)
    with open(temp, 'a', encoding='utf-8') as f:
      f.write('// EXTRA_INFO: ' + json.dumps(extra_info))
    filename = temp
  args = [filename, *passes]
  if not worker_js:
    # Keep JS code comments intact through the acorn optimization pass so that
    # JSDoc comments will be carried over to a later Closure run.
    if settings.MAYBE_CLOSURE_COMPILER:
      args += ['--closure-friendly']
    if settings.EXPORT_ES6:
      args += ['--export-es6']
  if settings.VERBOSE:
    args += ['--verbose']
  if return_output:
    return shared.run_js_tool(optimizer, args, stdout=PIPE)

  acorn_optimizer.counter += 1
  basename = utils.unsuffixed(original_filename)
  if '.jso' in basename:
    basename = utils.unsuffixed(basename)
  output_file = basename + f'.jso{acorn_optimizer.counter}.js'
  shared.get_temp_files().note(output_file)
  args += ['-o', output_file]
  shared.run_js_tool(optimizer, args)
  if not shared.SKIP_SUBPROCS:
    save_intermediate(output_file, f'{passes[0]}.js')
  return output_file


acorn_optimizer.counter = 0  # type: ignore

WASM_CALL_CTORS = '__wasm_call_ctors'


def eval_ctors(js_file, wasm_file, debug_info):
  CTOR_ADD_PATTERN = f"wasmExports['{WASM_CALL_CTORS}']();"

  js = utils.read_file(js_file)

  has_wasm_call_ctors = False

  # eval the ctor caller as well as main, or, in standalone mode, the proper
  # entry/init function
  if not settings.STANDALONE_WASM:
    ctors = []
    kept_ctors = []
    has_wasm_call_ctors = CTOR_ADD_PATTERN in js
    if has_wasm_call_ctors:
      ctors += [WASM_CALL_CTORS]
    if settings.HAS_MAIN:
      main = 'main'
      if '__main_argc_argv' in settings.WASM_EXPORTS:
        main = '__main_argc_argv'
      ctors += [main]
      # TODO perhaps remove the call to main from the JS? or is this an abi
      #      we want to preserve?
      kept_ctors += [main]
    if not ctors:
      logger.info('ctor_evaller: no ctors')
      return
    args = ['--ctors=' + ','.join(ctors)]
    if kept_ctors:
      args += ['--kept-exports=' + ','.join(kept_ctors)]
  else:
    if settings.EXPECT_MAIN:
      ctor = '_start'
    else:
      ctor = '_initialize'
    args = ['--ctors=' + ctor, '--kept-exports=' + ctor]
  if settings.EVAL_CTORS == 2:
    args += ['--ignore-external-input']
  logger.info('ctor_evaller: trying to eval global ctors (' + ' '.join(args) + ')')
  out = run_binaryen_command('wasm-ctor-eval', wasm_file, wasm_file, args=args, stdout=PIPE, debug=debug_info)
  logger.info('\n\n' + out)
  num_successful = out.count('success on')
  if num_successful and has_wasm_call_ctors:
    js = js.replace(CTOR_ADD_PATTERN, '')
    settings.WASM_EXPORTS.remove(WASM_CALL_CTORS)
  utils.write_file(js_file, js)


def get_closure_compiler():
  # First check if the user configured a specific CLOSURE_COMPILER in their settings
  if config.CLOSURE_COMPILER:
    return config.CLOSURE_COMPILER

  # Otherwise use the one installed via npm
  return shared.get_npm_cmd('google-closure-compiler')


def check_closure_compiler(cmd, args, env, allowed_to_fail):
  cmd = cmd + args + ['--version']
  try:
    output = run_process(cmd, stdout=PIPE, env=env).stdout
  except Exception as e:
    if allowed_to_fail:
      return False
    if isinstance(e, subprocess.CalledProcessError):
      sys.stderr.write(e.stdout)
    sys.stderr.write(str(e) + '\n')
    exit_with_error(f'closure compiler ({shlex.join(cmd)}) did not execute properly!')

  if 'Version:' not in output:
    if allowed_to_fail:
      return False
    exit_with_error(f'unrecognized closure compiler --version output ({shlex.join(cmd)}):\n{output}')

  return True


def get_closure_compiler_and_env(user_args):
  env = shared.env_with_node_in_path()
  # Work around an issue that Closure compiler can take up a lot of memory and crash in an error
  # "FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap
  # out of memory"
  env['NODE_OPTIONS'] = '--max_old_space_size=8192'
  closure_cmd = get_closure_compiler()

  native_closure_compiler_works = check_closure_compiler(closure_cmd, user_args, env, allowed_to_fail=True)
  if not native_closure_compiler_works and not any(a.startswith('--platform') for a in user_args):
    # Run with Java Closure compiler as a fallback if the native version does not work.
    # This can happen, for example, on arm64 macOS machines that do not have Rosetta installed.
    logger.debug('falling back to java version of closure compiler')
    user_args.append('--platform=java')
    check_closure_compiler(closure_cmd, user_args, env, allowed_to_fail=False)

  return closure_cmd, env


def version_split(v):
  """Split version setting number (e.g. 162000) into versions string (e.g. "16.2.0")."""
  v = str(v).rjust(6, '0')
  assert len(v) == 6
  m = re.match(r'(\d{2})(\d{2})(\d{2})', v)
  major, minor, rev = m.group(1, 2, 3)
  return f'{int(major)}.{int(minor)}.{int(rev)}'


def filter_closure_args(args):
  # Closure compiler includes WebGPU externs natively (w3c_webgpu.js).
  # Exclude webgpu-externs.js (provided by emdawnwebgpu) to avoid duplicate definition errors.
  # TODO: Remove this after https://g-issues.chromium.org/issues/562078433 is resolved.
  return [a for a in args if not (a.startswith('--externs=') and a.endswith('webgpu-externs.js'))]


@ToolchainProfiler.profile()
def closure_compiler(filename, advanced=True, extra_closure_args=None):
  user_args = []
  env_args = os.environ.get('EMCC_CLOSURE_ARGS')
  if env_args:
    user_args += shlex.split(env_args)
  if extra_closure_args:
    user_args += extra_closure_args
  if any('emdawnwebgpu' in p.name for p in ports.get_needed_ports(settings)):
    user_args = filter_closure_args(user_args)

  closure_cmd, env = get_closure_compiler_and_env(user_args)

  # Closure externs file contains known symbols to be extern to the minification, Closure
  # should not minify these symbol names.
  CLOSURE_EXTERNS = [path_from_root('src/closure-externs/closure-externs.js')]

  if settings.MODULARIZE:
    CLOSURE_EXTERNS += [path_from_root('src/closure-externs/modularize-externs.js')]

  if settings.AUDIO_WORKLET:
    CLOSURE_EXTERNS += [path_from_root('src/closure-externs/audio-worklet-externs.js')]

  # Closure compiler needs to know about all exports that come from the wasm module, because to optimize for small code size,
  # the exported symbols are added to global scope via a foreach loop in a way that evades Closure's static analysis. With an explicit
  # externs file for the exports, Closure is able to reason about the exports.
  if settings.WASM_EXPORTS and not settings.DECLARE_ASM_MODULE_EXPORTS:
    # Generate an exports file that records all the exported symbols from the wasm module.
    exports = [asmjs_mangle(i) for i in settings.WASM_EXPORTS] + settings.ALIASES
    module_exports_suppressions = '\n'.join([f'/**\n * @suppress {{duplicate, undefinedVars}}\n */\nvar {e};\n' for e in exports])
    exports_file = shared.get_temp_files().get('.js', prefix='emcc_module_exports_')
    exports_file.write(module_exports_suppressions.encode())
    exports_file.close()

    CLOSURE_EXTERNS += [exports_file.name]

  # Node.js specific externs
  if settings.ENVIRONMENT_MAY_BE_NODE:
    node_extern_dir = path_from_root('third_party/closure-compiler/node-externs')
    node_externs = os.listdir(node_extern_dir)
    node_externs = [os.path.join(node_extern_dir, name) for name in node_externs if name.endswith('.js')]
    CLOSURE_EXTERNS += [path_from_root('src/closure-externs/node-externs.js'), *node_externs]

  # V8/SpiderMonkey shell specific externs
  if settings.ENVIRONMENT_MAY_BE_SHELL:
    V8_EXTERNS = [path_from_root('src/closure-externs/v8-externs.js')]
    SPIDERMONKEY_EXTERNS = [path_from_root('src/closure-externs/spidermonkey-externs.js')]
    CLOSURE_EXTERNS += V8_EXTERNS + SPIDERMONKEY_EXTERNS

  if settings.DYNCALLS:
    CLOSURE_EXTERNS += [path_from_root('src/closure-externs/dyncall-externs.js')]

  args = ['--compilation_level', 'ADVANCED_OPTIMIZATIONS' if advanced else 'SIMPLE_OPTIMIZATIONS']
  args += ['--language_in', 'UNSTABLE']
  # Make Closure aware of the ES6 module syntax;
  # i.e. the `import.meta` and `await import` usages
  if settings.EXPORT_ES6:
    args += ['--chunk_output_type', 'ES_MODULES']
    if settings.ENVIRONMENT_MAY_BE_NODE:
      args += ['--module_resolution', 'NODE']
      # https://github.com/google/closure-compiler/issues/3740
      args += ['--jscomp_off=moduleLoad']
  # We currently only use closure compiler for minification, not transpilation.
  args += ['--language_out', 'NO_TRANSPILE']
  # Tell closure never to inject the 'use strict' directive.
  args += ['--emit_use_strict=false']
  args += ['--assume_static_inheritance_is_not_used=false']
  # Always output UTF-8 files, this helps generate UTF-8 code points instead of escaping code points with \uxxxx inside strings.
  # Closure outputs ASCII by default, and must be adjusted to output UTF8 (https://github.com/google/closure-compiler/issues/4158)
  args += ['--charset=UTF8']

  if settings.IGNORE_CLOSURE_COMPILER_ERRORS:
    args.append('--jscomp_off=*')
  # Specify input file relative to the temp directory to avoid specifying non-7-bit-ASCII path names.
  for e in CLOSURE_EXTERNS:
    args += ['--externs', e]
  args += user_args

  if settings.DEBUG_LEVEL > 1:
    args += ['--debug']

  # Now that we have run closure compiler once, we have stripped all the closure compiler
  # annotations from the source code and we no longer need to worry about generating closure
  # friendly code.
  # This means all the calls to acorn_optimizer that come after this will now run without
  # --closure-friendly
  settings.MAYBE_CLOSURE_COMPILER = False

  cmd = closure_cmd + args
  return run_closure_cmd(cmd, filename, env)


def run_closure_cmd(cmd, filename, env):
  cmd += ['--js', filename]

  # Closure compiler is unable to deal with path names that are not 7-bit ASCII:
  # https://github.com/google/closure-compiler/issues/3784
  tempfiles = shared.get_temp_files()

  def move_to_safe_7bit_ascii_filename(filename):
    if os.path.abspath(filename).isascii():
      return os.path.abspath(filename)
    safe_filename = tempfiles.get('.js').name  # Safe 7-bit filename
    shutil.copyfile(filename, safe_filename)
    return os.path.relpath(safe_filename, tempfiles.tmpdir)

  for i in range(len(cmd)):
    for prefix in ('--externs', '--js'):
      # Handle the case where the flag and the value are two separate arguments.
      if cmd[i] == prefix:
        cmd[i + 1] = move_to_safe_7bit_ascii_filename(cmd[i + 1])
      # and the case where they are one argument, e.g. --externs=foo.js
      elif cmd[i].startswith(prefix + '='):
        # Replace the argument with a version that has a safe filename.
        filename = cmd[i].split('=', 1)[1]
        cmd[i] = '='.join([prefix, move_to_safe_7bit_ascii_filename(filename)])

  outfile = tempfiles.get('.cc.js').name  # Safe 7-bit filename

  # Specify output file relative to the temp directory to avoid specifying non-7-bit-ASCII path names.
  cmd += ['--js_output_file', os.path.relpath(outfile, tempfiles.tmpdir)]
  if not settings.MINIFY_WHITESPACE:
    cmd += ['--formatting', 'PRETTY_PRINT']

  if settings.WASM2JS:
    # In WASM2JS mode, the WebAssembly object is polyfilled, which triggers
    # Closure's built-in type check:
    # externs.zip//webassembly.js:29:18: WARNING - [JSC_TYPE_MISMATCH] initializing variable
    # We cannot fix this warning externally, since adding /** @suppress{checkTypes} */
    # to the polyfill is "in the wrong end". So mute this warning globally to
    # allow clean Closure output. https://github.com/google/closure-compiler/issues/4108
    cmd += ['--jscomp_off=checkTypes']

    # WASM2JS codegen routinely generates expressions that are unused, e.g.
    # WARNING - [JSC_USELESS_CODE] Suspicious code. The result of the 'bitor' operator is not being used.
    #        s(0) | 0;
    #        ^^^^^^^^
    # Turn off this check in Closure to allow clean Closure output.
    cmd += ['--jscomp_off=uselessCode']

  shared.print_compiler_stage(cmd)

  # Closure compiler does not work if any of the input files contain characters outside the
  # 7-bit ASCII range. Therefore make sure the command line we pass does not contain any such
  # input files by passing all input filenames relative to the cwd. (user temp directory might
  # be in user's home directory, and user's profile name might contain unicode characters)
  # https://github.com/google/closure-compiler/issues/4159: Closure outputs stdout/stderr in iso-8859-1 on Windows.
  proc = run_process(cmd, stderr=PIPE, check=False, env=env, cwd=tempfiles.tmpdir, encoding='iso-8859-1' if WINDOWS else 'utf-8')

  # XXX Closure bug: if Closure is invoked with --create_source_map, Closure should create a
  # outfile.map source map file (https://github.com/google/closure-compiler/wiki/Source-Maps)
  # But it looks like it creates such files on Linux(?) even without setting that command line
  # flag (and currently we don't), so delete the produced source map file to not leak files in
  # temp directory.
  utils.delete_file(outfile + '.map')

  closure_warnings = diagnostics.manager.warnings['closure']

  # Print Closure diagnostics result up front.
  if proc.returncode != 0:
    logger.error('Closure compiler run failed:\n')
  elif len(proc.stderr.strip()) > 0 and closure_warnings['enabled']:
    if closure_warnings['error']:
      logger.error('Closure compiler completed with warnings and -Werror=closure enabled, aborting!\n')
    else:
      logger.warning('Closure compiler completed with warnings:\n')

  # Print input file (long wall of text!)
  if DEBUG == 2 and (proc.returncode != 0 or (len(proc.stderr.strip()) > 0 and closure_warnings['enabled'])):
    input_file = utils.read_file(filename).splitlines()
    for i in range(len(input_file)):
      sys.stderr.write(f'{i + 1}: {input_file[i]}\n')

  if proc.returncode != 0:
    logger.error(proc.stderr) # print list of errors (possibly long wall of text if input was minified)

    # Exit and print final hint to get clearer output
    msg = f'closure compiler failed (rc: {proc.returncode}): {shlex.join(cmd)}'
    if settings.MINIFY_WHITESPACE:
      msg += ' the error message may be clearer with -g1 and EMCC_DEBUG=2 set'
    exit_with_error(msg)

  if len(proc.stderr.strip()) > 0 and closure_warnings['enabled']:
    # print list of warnings (possibly long wall of text if input was minified)
    if closure_warnings['error']:
      logger.error(proc.stderr)
    else:
      logger.warning(proc.stderr)

    # Exit and/or print final hint to get clearer output
    if settings.MINIFY_WHITESPACE:
      logger.warning('(rerun with -g1 linker flag for an unminified output)')
    elif DEBUG != 2:
      logger.warning('(rerun with EMCC_DEBUG=2 enabled to dump Closure input file)')

    if closure_warnings['error']:
      exit_with_error('closure compiler produced warnings and -W=error=closure enabled')

  return outfile


# minify the final wasm+JS combination. this is done after all the JS
# and wasm optimizations; here we do the very final optimizations on them
def minify_wasm_js(js_file, wasm_file, expensive_optimizations, debug_info):
  passes = []
  extra_info = {}

  # if we can optimize this js+wasm combination under the assumption no one else
  # will see the internals, do so
  if not settings.LINKABLE:
    if expensive_optimizations:
      # start with AJSDCE (aggressive JS DCE, performs multiple iterations), to
      # clean up obvious JS garbage before metadce.
      logger.debug('running pre-meta-DCE cleanup on shell code: AJSDCE')
      js_file = acorn_optimizer(js_file, ['AJSDCE'])
      # if we are optimizing for size, shrink the combined wasm+JS
      # TODO: support this when a symbol map is used
      dce_info = metadce(js_file,
                         wasm_file,
                         debug_info=debug_info,
                         last=not settings.MINIFY_WASM_IMPORTS_AND_EXPORTS)
      if dce_info:
        # now that we removed unneeded communication between js and wasm, we can
        # clean up the js some more.
        passes += ['applyDCEGraphRemovals', 'AJSDCE']
        extra_info.update(dce_info)
      if settings.MINIFY_WASM_IMPORTS_AND_EXPORTS:
        mapping = minify_wasm_imports_and_exports(wasm_file,
                                                  minify_exports=settings.MINIFY_WASM_EXPORT_NAMES,
                                                  debug_info=debug_info)
        passes.append('applyImportAndExportNameChanges')
        extra_info['mapping'] = mapping
    else:
      passes.append('JSDCE')

  # Don't minify if we are going to run closure compiler afterwards
  if settings.MINIFY_WHITESPACE and not settings.MAYBE_CLOSURE_COMPILER:
    passes.append('--minify-whitespace')
    passes.append('--compress')

  if passes:
    logger.debug('running cleanup on shell code: ' + ' '.join(passes))
    js_file = acorn_optimizer(js_file, passes, extra_info=extra_info or None)

  return js_file


# get the flags to pass into the very last binaryen tool invocation, that runs
# the final set of optimizations
def get_last_binaryen_opts():
  return [f'--optimize-level={settings.OPT_LEVEL}',
          f'--shrink-level={settings.SHRINK_LEVEL}',
          '--optimize-stack-ir']


# run binaryen's wasm-metadce to dce both js and wasm
def metadce(js_file, wasm_file, debug_info, last):
  logger.debug('running meta-DCE')
  temp_files = shared.get_temp_files()
  extra_info = {"exports": [[asmjs_mangle(x), x] for x in settings.WASM_EXPORTS]}

  txt = acorn_optimizer(js_file, ['emitDCEGraph', '--no-print'], return_output=True, extra_info=extra_info)
  if shared.SKIP_SUBPROCS:
    # The next steps depend on the output from this step, so we can't do them if we aren't actually running.
    return None
  graph = json.loads(txt)
  # ensure that functions expected to be exported to the outside are roots
  required_symbols = user_requested_exports.union(extra_js_exports, settings.SIDE_MODULE_IMPORTS)
  for item in graph:
    if 'export' in item:
      export = asmjs_mangle(item['export'])
      if settings.EXPORT_ALL or export in required_symbols:
        item['root'] = True

  # fix wasi imports TODO: support wasm stable with an option?
  WASI_IMPORTS = {
    'environ_get',
    'environ_sizes_get',
    'args_get',
    'args_sizes_get',
    'fd_write',
    'fd_close',
    'fd_read',
    'fd_seek',
    'fd_fdstat_get',
    'fd_sync',
    'fd_pread',
    'fd_pwrite',
    'proc_exit',
    'clock_res_get',
    'clock_time_get',
    'path_open',
    'random_get',
  }
  for item in graph:
    if 'import' in item and item['import'][1] in WASI_IMPORTS:
      item['import'][0] = settings.WASI_MODULE_NAME

  # map import/export names to native wasm symbols.
  import_name_map = {}
  export_name_map = {}
  for item in graph:
    if 'import' in item:
      name = item['import'][1]
      import_name_map[item['name']] = name
      if asmjs_mangle(name) in settings.SIDE_MODULE_IMPORTS:
        item['root'] = True
    elif 'export' in item:
      export_name_map[item['name']] = item['export']
  temp = temp_files.get('.json', prefix='emcc_dce_graph_').name
  utils.write_file(temp, json.dumps(graph, indent=2))
  # run wasm-metadce
  args = ['--graph-file=' + temp]
  if last:
    args += get_last_binaryen_opts()
  out = run_binaryen_command('wasm-metadce',
                             wasm_file,
                             wasm_file,
                             args,
                             debug=debug_info,
                             stdout=PIPE)
  # find the unused things in js
  unused_imports = []
  unused_exports = []
  for line in out.splitlines():
    if line.startswith('unused:'):
      name = line.removeprefix('unused:').strip()
      # With dynamic linking we never want to strip the memory or the table
      # This can be removed once SIDE_MODULE_IMPORTS includes tables and memories.
      if settings.MAIN_MODULE and name.split('$')[-1] in {'wasmMemory', 'wasmTable'}:
        continue
      # we only remove imports and exports in applyDCEGraphRemovals
      if name.startswith('emcc$import$'):
        native_name = import_name_map[name]
        unused_imports.append(native_name)
      elif name.startswith('emcc$export$') and settings.DECLARE_ASM_MODULE_EXPORTS:
        native_name = export_name_map[name]
        # Internal/system exports (e.g. memory, __asyncify_data, dynCall_*, etc.)
        # do not have standard JS receiving assignments (_name = wasmExports['name']),
        # so including them in unused_exports would fail applyDCEGraphRemovals's assertion.
        # However, under WASM_ESM_INTEGRATION the JS receives every wasm export
        # as an ES import, so any export binaryen drops (including internal ones)
        # must also be dropped from the JS import to keep the two module
        # interfaces in sync.
        if not shared.is_internal_symbol(native_name) or settings.WASM_ESM_INTEGRATION:
          unused_exports.append(native_name)
  if not unused_exports and not unused_imports:
    # nothing found to be unused, so we have nothing to remove
    return None
  if DEBUG:
    logger.debug(f'unused_imports: {unused_imports}')
    logger.debug(f'unused_exports: {unused_exports}')
  return {'unusedImports': unused_imports, 'unusedExports': unused_exports}


def minify_wasm_imports_and_exports(wasm_file, minify_exports, debug_info):
  logger.debug('minifying wasm imports and exports')
  # run the pass
  args = []
  if minify_exports:
    # standalone wasm mode means we need to emit a wasi import module.
    # otherwise, minify even the imported module names.
    if settings.MINIFY_WASM_IMPORTED_MODULES:
      args.append('--minify-imports-and-exports-and-modules')
    else:
      args.append('--minify-imports-and-exports')
  else:
    args.append('--minify-imports')
  # this is always the last tool we run (if we run it)
  args += get_last_binaryen_opts()
  out = run_wasm_opt(wasm_file, wasm_file, args, debug=debug_info, stdout=PIPE)

  # Get the mapping. This can be in the old format, which is lines of
  #
  # OLD => NEW
  #
  # Or, it can be the new format:
  #
  # {
  #   "imports": [
  #     [OLD_MODULE, OLD, NEW],
  #     ..
  #   },
  #   "exports": [
  #     [OLD, NEW],
  #     ..
  #   }
  # }
  #
  # We differentiate them by the first character (if present).
  #
  # TODO: Remove the old format eventually after the new one rolls in.
  mapping = {}
  if not out or out[0] != '{':
    SEP = ' => '
    for line in out.split('\n'):
      if SEP in line:
        old, new = line.strip().split(SEP)
        assert old not in mapping, 'imports must be unique'
        mapping[old] = new
  else:
    parsed = json.loads(out)
    for imp in parsed['imports']:
      # the module name is ignored; we assume no collisions can happen there
      _module, old, new = imp
      assert old not in mapping, 'imports must be unique'
      mapping[old] = new
    for exp in parsed['exports']:
      old, new = exp
      assert old not in mapping, 'exports must be unique'
      mapping[old] = new

  if settings.MINIFICATION_MAP:
    lines = [f'{new}:{old}' for old, new in mapping.items()]
    utils.write_file(settings.MINIFICATION_MAP, '\n'.join(lines) + '\n')
  return mapping


def wasm2js(js_file, wasm_file, opt_level, use_closure_compiler, debug_info, symbols_file=None, symbols_file_js=None):
  logger.debug('wasm2js')
  args = ['--emscripten']
  if opt_level > 0:
    args += ['-O']
  if symbols_file:
    args += [f'--symbols-file={symbols_file}']
  wasm2js_js = run_binaryen_command('wasm2js', wasm_file,
                                    args=args,
                                    debug=debug_info,
                                    stdout=PIPE)
  if DEBUG:
    utils.write_file(os.path.join(get_emscripten_temp_dir(), 'wasm2js-output.js'), wasm2js_js)
  # JS optimizations
  if opt_level >= 2:
    passes = []
    if not debug_info and not settings.PTHREADS:
      passes += ['minifyNames']
      if symbols_file_js:
        passes += [f'symbolMap={symbols_file_js}']
    if settings.MINIFY_WHITESPACE:
      passes += ['--minify-whitespace']
    if passes:
      # hackish fixups to work around wasm2js style and the js optimizer FIXME
      wasm2js_js = f'// EMSCRIPTEN_START_ASM\n{wasm2js_js}// EMSCRIPTEN_END_ASM\n'
      wasm2js_js = wasm2js_js.replace('\n function $', '\nfunction $')
      wasm2js_js = wasm2js_js.replace('\n }', '\n}')
      temp = shared.get_temp_files().get('.js').name
      utils.write_file(temp, wasm2js_js)
      temp = run_js_optimizer(temp, passes)
      wasm2js_js = utils.read_file(temp)
  # Closure compiler: in mode 1, we just minify the shell. In mode 2, we
  # minify the wasm2js output as well.
  # TODO: in the non-closure case, we could run a lightweight general-
  #       purpose JS minifier here.
  if use_closure_compiler == 2:
    temp = shared.get_temp_files().get('.js').name
    with open(temp, 'a', encoding='utf-8') as f:
      f.write(wasm2js_js)
    temp = closure_compiler(temp, advanced=False)
    wasm2js_js = utils.read_file(temp)
    # closure may leave a trailing `;`, which would be invalid given where we place
    # this code (inside parens)
    wasm2js_js = wasm2js_js.strip()
    if wasm2js_js[-1] == ';':
      wasm2js_js = wasm2js_js[:-1]
  all_js = utils.read_file(js_file)
  # quoted notation, something like Module['__wasm2jsInstantiate__']
  finds = re.findall(r'''[\w\d_$]+\[['"]__wasm2jsInstantiate__['"]\]''', all_js)
  if not finds:
    # post-closure notation, something like a.__wasm2jsInstantiate__
    finds = re.findall(r'''[\w\d_$]+\.__wasm2jsInstantiate__''', all_js)
  assert len(finds) == 1
  marker = finds[0]
  all_js = all_js.replace(marker, f'(\n{wasm2js_js}\n)')
  # replace the placeholder with the actual code
  js_file += '.wasm2js.js'
  utils.write_file(js_file, all_js)
  return js_file


@ToolchainProfiler.profile()
def strip_sections(infile, outfile, sections):
  """Strip specified sections from a wasm file."""
  cmd = [LLVM_OBJCOPY, infile, outfile] + ['--remove-section=' + section for section in sections]
  check_call(cmd)


# extract the DWARF info from the main file, and leave the wasm with
# debug info as a file on the side
def emit_debug_on_side(wasm_file, wasm_file_with_dwarf):
  embedded_path = settings.SEPARATE_DWARF_URL
  if not embedded_path:
    # a path was provided - make it relative to the wasm.
    embedded_path = os.path.relpath(wasm_file_with_dwarf,
                                    os.path.dirname(wasm_file))
    # normalize the path to use URL-style separators, per the spec
    embedded_path = utils.normalize_path(embedded_path)

  shutil.move(wasm_file, wasm_file_with_dwarf)
  strip_sections(wasm_file_with_dwarf, wasm_file, ['.debug*'])

  # Strip code and data from the debug file to limit its size. The other known
  # sections are still required to correctly interpret the DWARF info.
  # TODO(dschuff): Also strip the DATA section? To make this work we'd need to
  # either allow "invalid" data segment name entries, or maybe convert the DATA
  # to a DATACOUNT section.
  # TODO(https://github.com/emscripten-core/emscripten/issues/13084): Re-enable
  # this code once the debugger extension can handle wasm files with name
  # sections but no code sections.
  # strip_sections(wasm_file_with_dwarf, wasm_file_with_dwarf, ['CODE'])

  # embed a section in the main wasm to point to the file with external DWARF,
  # see https://yurydelendik.github.io/webassembly-dwarf/#external-DWARF
  section_name = b'\x13external_debug_info' # section name, including prefixed size
  filename_bytes = embedded_path.encode('utf-8')
  contents = webassembly.to_leb(len(filename_bytes)) + filename_bytes
  section_size = len(section_name) + len(contents)
  with open(wasm_file, 'ab') as f:
    f.write(b'\0') # user section is code 0
    f.write(webassembly.to_leb(section_size))
    f.write(section_name)
    f.write(contents)


def little_endian_heap(js_file):
  logger.debug('enforcing little endian heap byte order')
  return acorn_optimizer(js_file, ['littleEndianHeap'])


def apply_wasm_memory_growth(js_file):
  assert settings.GROWABLE_ARRAYBUFFERS != 2
  logger.debug('supporting wasm memory growth with pthreads')
  return acorn_optimizer(js_file, ['growableHeap'])


def use_unsigned_pointers_in_js(js_file):
  logger.debug('using unsigned pointers in JS')
  return acorn_optimizer(js_file, ['unsignPointers'])


def instrument_js_for_asan(js_file):
  logger.debug('instrumenting JS memory accesses for ASan')
  return acorn_optimizer(js_file, ['asanify'])


def instrument_js_for_safe_heap(js_file):
  logger.debug('instrumenting JS memory accesses for SAFE_HEAP')
  return acorn_optimizer(js_file, ['safeHeap'])


def read_name_section(wasm_file):
  with webassembly.Module(wasm_file) as module:
    for section in module.sections():
      if section.type == webassembly.SecType.CUSTOM:
        module.seek(section.offset)
        if module.read_string() == 'name':
          name_map = {}
          # The name section is made up of sub-sections.
          # We are looking for the function names sub-section
          while module.tell() < section.offset + section.size:
            name_type = module.read_uleb()
            subsection_size = module.read_uleb()
            subsection_end = module.tell() + subsection_size
            if name_type == webassembly.NameType.FUNCTION:
              # We found the function names sub-section
              num_names = module.read_uleb()
              for _ in range(num_names):
                id = module.read_uleb()
                name = module.read_string()
                name_map[id] = name
              return name_map
            module.seek(subsection_end)

          return name_map


@ToolchainProfiler.profile()
def write_symbol_map(wasm_file, symbols_file):
  logger.debug('handle_final_wasm_symbols')
  names = read_name_section(wasm_file)
  assert names
  strings = [f'{id}:{name}' for id, name in names.items()]
  contents = '\n'.join(strings) + '\n'
  utils.write_file(symbols_file, contents)


def emit_wasm_source_map(wasm_file, map_file, final_wasm):
  # source file paths must be relative to the location of the map (which is
  # emitted alongside the wasm)
  base_path = os.path.dirname(os.path.abspath(final_wasm))

  # TODO(sbc): Rename wasm-sourcemap so it can be imported directly without
  # importlib.
  wasm_sourcemap = importlib.import_module('tools.wasm-sourcemap')
  sourcemap_cmd = [wasm_file,
                   '--dwarfdump=' + LLVM_DWARFDUMP,
                   '-o', map_file,
                   '--basepath=' + base_path]

  if settings.SOURCE_MAP_PREFIXES:
    sourcemap_cmd += ['--prefix', *settings.SOURCE_MAP_PREFIXES]

  if settings.GENERATE_SOURCE_MAP == 2:
    sourcemap_cmd += ['--sources']

  # TODO(sbc): Convert to using library internal API instead of running `main` here
  rtn = wasm_sourcemap.main(sourcemap_cmd)
  if rtn != 0:
    exit_with_error('wasm-sourcemap failed (%s)', sourcemap_cmd)


def get_binaryen_feature_flags():
  # `binaryen_features` is empty unless features have been extracted by
  # a previous call to a binaryen tool.
  if binaryen_features:
    return binaryen_features
  else:
    return ['--detect-features']


def get_binaryen_version(bindir):
  opt = utils.find_exe(bindir, 'wasm-opt')
  if not os.path.exists(opt):
    exit_with_error(f'binaryen executable not found ({opt}). Please check your binaryen installation')
  try:
    return run_process([opt, '--version'], stdout=PIPE).stdout
  except subprocess.CalledProcessError:
    exit_with_error(f'error running binaryen executable ({opt}). Please check your binaryen installation')


def check_binaryen(bindir):
  output = get_binaryen_version(bindir)
  if output:
    output = output.splitlines()[0]
  try:
    version = output.split()[2]
    version = int(version)
  except (IndexError, ValueError):
    exit_with_error(f'error parsing binaryen version ({output}). Please check your binaryen installation')

  if version == EXPECTED_BINARYEN_VERSION:
    return True
  # When running in CI environment we also silently allow the next major
  # version of binaryen here so that new versions of binaryen can be rolled in
  # without disruption.
  if 'BUILDBOT_BUILDNUMBER' in os.environ:
    if version == EXPECTED_BINARYEN_VERSION + 1:
      return True
  diagnostics.warning('version-check', 'unexpected binaryen version: %s (expected %s)', version, EXPECTED_BINARYEN_VERSION)
  return False


def get_binaryen_bin():
  global binaryen_checked
  rtn = os.path.join(config.BINARYEN_ROOT, 'bin')
  if not binaryen_checked:
    check_binaryen(rtn)
    binaryen_checked = True
  return rtn


# track whether the last binaryen command kept debug info around. this is used
# to see whether we need to do an extra step at the end to strip it.
binaryen_kept_debug_info = False


def run_binaryen_command(tool, infile, outfile=None, args=None, debug=False, stdout=None):
  cmd = [os.path.join(get_binaryen_bin(), tool)]
  if args:
    cmd += args
  if infile:
    cmd += [infile]
  if outfile:
    cmd += ['-o', outfile]
    if settings.ERROR_ON_WASM_CHANGES_AFTER_LINK:
      # emit some extra helpful text for common issues
      extra = ''
      if settings.OPT_LEVEL > 1:
        extra += '\nnote: -O2+ optimizations always require changes, build with -O0 or -O1 instead'
      exit_with_error(f'changes to the wasm are required after link, but disallowed by ERROR_ON_WASM_CHANGES_AFTER_LINK: {cmd}{extra}')
  if debug:
    cmd += ['-g'] # preserve the debug info
  # if the features are not already handled, handle them
  cmd += get_binaryen_feature_flags()
  # if we are emitting a source map, every time we load and save the wasm
  # we must tell binaryen to update it
  # TODO: all tools should support source maps; wasm-ctor-eval does not atm,
  #       for example
  if settings.GENERATE_SOURCE_MAP and outfile and tool in {'wasm-opt', 'wasm-emscripten-finalize', 'wasm-metadce'}:
    cmd += [f'--input-source-map={infile}.map']
    cmd += [f'--output-source-map={outfile}.map']
  if shared.SKIP_SUBPROCS:
    shared.print_compiler_stage(cmd)
    return ''
  ret = check_call(cmd, stdout=stdout).stdout
  if outfile:
    save_intermediate(outfile, f'{tool}.wasm')
    global binaryen_kept_debug_info
    binaryen_kept_debug_info = '-g' in cmd
  return ret


def run_wasm_opt(infile, outfile=None, args=[], **kwargs):  # ruff: ignore[mutable-argument-default]
  return run_binaryen_command('wasm-opt', infile, outfile, args=args, **kwargs)


def has_wasm_bindgen_marker(input_files):
  if not input_files:
    return False
  result = check_call([LLVM_OBJDUMP, '--section-headers', *input_files], stdout=PIPE)
  return '__wasm_bindgen_emscripten_marker' in result.stdout


def run_wasm_bindgen(infile):
  bindgen_out_dir = os.path.join(get_emscripten_temp_dir(), 'bindgen_out')

  wasm_bindgen_bin = shutil.which('wasm-bindgen')
  if not wasm_bindgen_bin:
    exit_with_error('wasm-bindgen executable not found in $PATH')
  cmd = [
    wasm_bindgen_bin,
    infile,
    '--keep-lld-exports',
    '--keep-debug',
    '--out-dir',
    bindgen_out_dir,
  ]
  exports_before = {e.name for e in webassembly.get_exports(infile)}

  check_call(cmd)

  # Don't try to predict the .wasm filename that wasm-bindgen outputs. Instead
  # just grab the .wasm file itself.
  all_output_files = os.listdir(bindgen_out_dir)
  new_wasm_file = next(x for x in all_output_files if x.endswith('.wasm'))
  new_wasm_path = os.path.join(bindgen_out_dir, new_wasm_file)

  exports_after = {e.name for e in webassembly.get_exports(new_wasm_path)}
  removed_exports = exports_before - exports_after
  added_exports = exports_after - exports_before

  shutil.copyfile(new_wasm_path, infile)

  # Only emitted when the crate imports JS snippets.
  extern_pre_js = os.path.join(bindgen_out_dir, 'library_bindgen.extern-pre.js')
  if not os.path.exists(extern_pre_js):
    extern_pre_js = None
  snippets_dir = os.path.join(bindgen_out_dir, 'snippets')
  if not os.path.isdir(snippets_dir):
    snippets_dir = None

  return os.path.join(bindgen_out_dir, 'library_bindgen.js'), removed_exports, added_exports, extern_pre_js, snippets_dir


intermediate_counter = 0


def new_intermediate_filename(name):
  assert DEBUG
  global intermediate_counter
  basename = f'emcc-{intermediate_counter:02d}-{name}'
  intermediate_counter += 1
  filename = os.path.join(shared.CANONICAL_TEMP_DIR, basename)
  logger.debug(f'saving intermediate file {filename}')
  return filename


def save_intermediate(src, name):
  """Copy an existing file CANONICAL_TEMP_DIR."""
  if DEBUG:
    shutil.copyfile(src, new_intermediate_filename(name))


def write_intermediate(content, name):
  """Generate a new debug file CANONICAL_TEMP_DIR."""
  if DEBUG:
    utils.write_file(new_intermediate_filename(name), content)


def read_and_preprocess(filename, expand_macros=False):
  # Create a settings file with the current settings to pass to the JS preprocessor
  settings_json = json.dumps(settings.external_dict(), sort_keys=True, indent=2)
  write_intermediate(settings_json, 'settings.json')

  # Run the JS preprocessor
  dirname, filename = os.path.split(filename)
  if not dirname:
    dirname = None
  args = ['-', filename]
  if expand_macros:
    args += ['--expand-macros']

  return shared.run_js_tool(path_from_root('tools/preprocessor.mjs'), args, input=settings_json, stdout=subprocess.PIPE, cwd=dirname)


def js_legalization_pass_flags():
  flags = []
  if settings.SIDE_MODULE or settings.MAIN_MODULE:
    # When building in relocatable mode, we also want access the original
    # non-legalized wasm functions (since wasm modules can and do link to
    # the original, non-legalized, functions).
    flags += ['--pass-arg=legalize-js-interface-export-originals']
  if not settings.SIDE_MODULE:
    # Unless we are building a side module the helper functions should be
    # assumed to be defined and exports within the module, otherwise binaryen
    # assumes they are imports.
    flags += ['--pass-arg=legalize-js-interface-exported-helpers']
  return flags