# 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, config, diagnostics, js_optimizer, response_file, shared, utils, webassembly, ) from .feature_matrix import UNSUPPORTED from .settings import settings from .shared import ( CLANG_CC, CLANG_CXX, DEBUG, EMAR, EMCC, EMRANLIB, EMXX, LLVM_DWARFDUMP, LLVM_NM, LLVM_OBJCOPY, WASM_LD, 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 = 128 _is_ar_cache: dict[str, bool] = {} # the exports the user requested user_requested_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'] = EMCC 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 not settings.DISABLE_EXCEPTION_CATCHING: args += ['-enable-emscripten-cxx-exceptions'] 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('%s: %s' % (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 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 settings.LINKABLE: cmd.append('--export-dynamic') if settings.LTO and 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') 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: cmd.append('-Bdynamic') 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('--experimental-pic') 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') if not settings.LINKABLE: cmd.append('--no-export-dynamic') else: cmd.append('--export-table') if settings.ALLOW_TABLE_GROWTH: cmd.append('--growable-table') if not settings.SIDE_MODULE: cmd += ['-z', 'stack-size=%s' % settings.STACK_SIZE] if settings.ALLOW_MEMORY_GROWTH: cmd += ['--max-memory=%d' % settings.MAXIMUM_MEMORY] else: cmd += ['--no-growable-memory'] if settings.INITIAL_HEAP != -1: cmd += ['--initial-heap=%d' % settings.INITIAL_HEAP] if settings.INITIAL_MEMORY != -1: cmd += ['--initial-memory=%d' % 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'] else: if 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('--table-base=%s' % settings.TABLE_BASE) if not settings.STACK_FIRST: cmd.append('--global-base=%s' % settings.GLOBAL_BASE) return cmd 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] if settings.WASM_EXCEPTIONS: args += ['-mllvm', '-wasm-enable-eh'] if settings.WASM_EXCEPTIONS or settings.SUPPORT_LONGJMP == 'wasm': if settings.WASM_LEGACY_EXCEPTIONS: args += ['-mllvm', '-wasm-use-legacy-eh'] else: args += ['-mllvm', '-wasm-use-legacy-eh=0'] if settings.WASM_EXCEPTIONS or settings.SUPPORT_LONGJMP == 'wasm': args += ['-mllvm', '-exception-model=wasm'] 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) check_call(cmd) 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 shell wrappers (e.g. emcc.bat), 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('using response file for %s' % cmd[0]) filename = response_file.create_response_file(cmd[1:], shared.TEMP_DIR) new_cmd = [cmd[0], "@" + filename] return new_cmd def emar(action, output_filename, filenames, stdout=None, stderr=None, env=None): utils.delete_file(output_filename) cmd = [EMAR, action, output_filename] + filenames cmd = get_command_with_possible_response_file(cmd) run_process(cmd, stdout=stdout, stderr=stderr, env=env) if 'c' in action: assert os.path.exists(output_filename), 'emar could not create output file: ' + output_filename 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) # run JS optimizer on some JS, ignoring asm.js contents if any - just run on it all 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 cmd = config.NODE_JS + [optimizer, 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: cmd += ['--closure-friendly'] if settings.EXPORT_ES6: cmd += ['--export-es6'] if settings.VERBOSE: cmd += ['--verbose'] if return_output: if shared.SKIP_SUBPROCS: shared.print_compiler_stage(cmd) return '' return check_call(cmd, stdout=PIPE).stdout acorn_optimizer.counter += 1 basename = utils.unsuffixed(original_filename) if '.jso' in basename: basename = utils.unsuffixed(basename) output_file = basename + '.jso%d.js' % acorn_optimizer.counter shared.get_temp_files().note(output_file) cmd += ['-o', output_file] if shared.SKIP_SUBPROCS: shared.print_compiler_stage(cmd) return output_file check_call(cmd) save_intermediate(output_file, '%s.js' % passes[0]) return output_file acorn_optimizer.counter = 0 # type: ignore WASM_CALL_CTORS = '__wasm_call_ctors' # evals ctors. if binaryen_bin is provided, it is the dir of the binaryen tool # for this, and we are in wasm mode 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 cmd = shared.get_npm_cmd('google-closure-compiler') if not WINDOWS: # 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" cmd.insert(-1, '--max_old_space_size=8192') return cmd 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('closure compiler (%s) did not execute properly!' % shlex.join(cmd)) if 'Version:' not in output: if allowed_to_fail: return False exit_with_error('unrecognized closure compiler --version output (%s):\n%s' % (shlex.join(cmd), output)) return True def get_closure_compiler_and_env(user_args): env = shared.env_with_node_in_path() 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.warning('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)}' @ToolchainProfiler.profile() def transpile(filename): config = { 'sourceType': 'script', 'presets': ['@babel/preset-env'], 'targets': {}, } if settings.MIN_CHROME_VERSION != UNSUPPORTED: config['targets']['chrome'] = str(settings.MIN_CHROME_VERSION) if settings.MIN_FIREFOX_VERSION != UNSUPPORTED: config['targets']['firefox'] = str(settings.MIN_FIREFOX_VERSION) if settings.MIN_SAFARI_VERSION != UNSUPPORTED: config['targets']['safari'] = version_split(settings.MIN_SAFARI_VERSION) if settings.MIN_NODE_VERSION != UNSUPPORTED: config['targets']['node'] = version_split(settings.MIN_NODE_VERSION) config_json = json.dumps(config, indent=2) outfile = shared.get_temp_files().get('babel.js').name config_file = shared.get_temp_files().get('babel_config.json').name logger.debug(config_json) utils.write_file(config_file, config_json) cmd = shared.get_npm_cmd('babel') + [filename, '-o', outfile, '--config-file', config_file] # Babel needs access to `node_modules` for things like `preset-env`, but the # location of the config file (and the current working directory) might not be # in the emscripten tree, so we explicitly set NODE_PATH here. env = shared.env_with_node_in_path() env['NODE_PATH'] = path_from_root('node_modules') check_call(cmd, env=env) return outfile @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 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 and settings.ENVIRONMENT_MAY_BE_WEB and not settings.EXPORT_ES6: 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(['/**\n * @suppress {duplicate, undefinedVars}\n */\nvar %s;\n' % e 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_EXTERNS_BASE = path_from_root('third_party/closure-compiler/node-externs') NODE_EXTERNS = os.listdir(NODE_EXTERNS_BASE) NODE_EXTERNS = [os.path.join(NODE_EXTERNS_BASE, 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 # Web environment specific externs if settings.ENVIRONMENT_MAY_BE_WEB or settings.ENVIRONMENT_MAY_BE_WORKER: BROWSER_EXTERNS_BASE = path_from_root('src/closure-externs/browser-externs') if os.path.isdir(BROWSER_EXTERNS_BASE): BROWSER_EXTERNS = os.listdir(BROWSER_EXTERNS_BASE) BROWSER_EXTERNS = [os.path.join(BROWSER_EXTERNS_BASE, name) for name in BROWSER_EXTERNS if name.endswith('.js')] CLOSURE_EXTERNS += BROWSER_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'] # We do transpilation using babel 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): # start with JSDCE, to clean up obvious JS garbage. When optimizing for size, # use AJSDCE (aggressive JS DCE, performs multiple iterations). Clean up # whitespace if necessary too. passes = [] if not settings.LINKABLE: passes.append('JSDCE' if not expensive_optimizations else 'AJSDCE') # Don't minify if we are going to run closure compiler afterwards minify = settings.MINIFY_WHITESPACE and not settings.MAYBE_CLOSURE_COMPILER if minify: passes.append('--minify-whitespace') if passes: logger.debug('running cleanup on shell code: ' + ' '.join(passes)) js_file = acorn_optimizer(js_file, passes) # 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 we are optimizing for size, shrink the combined wasm+JS # TODO: support this when a symbol map is used if expensive_optimizations: js_file = metadce(js_file, wasm_file, debug_info=debug_info, last=not settings.MINIFY_WASM_IMPORTS_AND_EXPORTS) # now that we removed unneeded communication between js and wasm, we can clean up # the js some more. passes = ['AJSDCE'] if minify: passes.append('--minify-whitespace') logger.debug('running post-meta-DCE cleanup on shell code: ' + ' '.join(passes)) js_file = acorn_optimizer(js_file, passes) if settings.MINIFY_WASM_IMPORTS_AND_EXPORTS: js_file = minify_wasm_imports_and_exports(js_file, wasm_file, minify_exports=settings.MINIFY_WASM_EXPORT_NAMES, debug_info=debug_info) 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 js_file graph = json.loads(txt) # ensure that functions expected to be exported to the outside are roots required_symbols = user_requested_exports.union(set(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] if shared.is_user_export(native_name): 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 js_file # remove them passes = ['applyDCEGraphRemovals'] if settings.MINIFY_WHITESPACE: passes.append('--minify-whitespace') if DEBUG: logger.debug("unused_imports: %s", str(unused_imports)) logger.debug("unused_exports: %s", str(unused_exports)) extra_info = {'unusedImports': unused_imports, 'unusedExports': unused_exports} return acorn_optimizer(js_file, passes, extra_info=extra_info) def minify_wasm_imports_and_exports(js_file, 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 # apply them passes = ['applyImportAndExportNameChanges'] if settings.MINIFY_WHITESPACE: passes.append('--minify-whitespace') extra_info = {'mapping': mapping} 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 acorn_optimizer(js_file, passes, extra_info=extra_info) 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 += ['--symbols-file=%s' % 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 += ['symbolMap=%s' % 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, which is ok since it isn't # validating asm.js. # 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 not settings.GROWABLE_ARRAYBUFFERS 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 is_ar(filename): """Return True if the given filename is an ar archive, False otherwise.""" try: header = open(filename, 'rb').read(8) except Exception as e: logger.debug('is_ar failed to test whether file \'%s\' is a llvm archive file! Failed on exception: %s' % (filename, e)) return False return header in {b'!\n', b'!\n'} def is_wasm(filename): if not os.path.isfile(filename): return False header = open(filename, 'rb').read(webassembly.HEADER_SIZE) return header == webassembly.MAGIC + webassembly.VERSION def is_wasm_dylib(filename): """Detect wasm dynamic libraries by the presence of the "dylink" custom section.""" if not is_wasm(filename): return False with webassembly.Module(filename) as module: section = next(module.sections()) if section.type == webassembly.SecType.CUSTOM: module.seek(section.offset) if module.read_string() in {'dylink', 'dylink.0'}: return True return False 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('binaryen executable not found (%s). Please check your binaryen installation' % opt) try: return run_process([opt, '--version'], stdout=PIPE).stdout except subprocess.CalledProcessError: exit_with_error('error running binaryen executable (%s). Please check your binaryen installation' % opt) 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') # Allow the expected version or the following one in order avoid needing to update both # emscripten and binaryen in lock step in emscripten-releases. if version not in {EXPECTED_BINARYEN_VERSION, EXPECTED_BINARYEN_VERSION + 1}: diagnostics.warning('version-check', 'unexpected binaryen version: %s (expected %s)', version, EXPECTED_BINARYEN_VERSION) 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 = '' # a plain -O0 build *almost* doesn't need post-link changes, except for # legalization. show a clear error for those (as the flags the user passed # in are not enough to see what went wrong) if settings.LEGALIZE_JS_FFI: extra += '\nnote: to disable int64 legalization (which requires changes after link) use -sWASM_BIGINT' 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, '%s.wasm' % tool) global binaryen_kept_debug_info binaryen_kept_debug_info = '-g' in cmd return ret def run_wasm_opt(infile, outfile=None, args=[], **kwargs): # noqa return run_binaryen_command('wasm-opt', infile, outfile, args=args, **kwargs) intermediate_counter = 0 def new_intermediate_filename(name): assert DEBUG global intermediate_counter basename = 'emcc-%02d-%s' % (intermediate_counter, name) intermediate_counter += 1 filename = os.path.join(shared.CANONICAL_TEMP_DIR, basename) logger.debug('saving intermediate file %s' % 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