from .toolchain_profiler import ToolchainProfiler
import json
import logging
import os
import re
import shlex
import shutil
import subprocess
import sys
from typing import Set, Dict
from subprocess import PIPE
from . import cache
from . import diagnostics
from . import response_file
from . import shared
from . import webassembly
from . import config
from . import utils
from .shared import CLANG_CC, CLANG_CXX
from .shared import LLVM_NM, EMCC, EMAR, EMXX, EMRANLIB, WASM_LD
from .shared import LLVM_OBJCOPY
from .shared import run_process, check_call, exit_with_error
from .shared import path_from_root
from .shared import asmjs_mangle, DEBUG
from .shared import LLVM_DWARFDUMP, demangle_c_symbol_name
from .shared import get_emscripten_temp_dir, exe_suffix, is_c_symbol
from .utils import WINDOWS
from .settings import settings
from .feature_matrix import UNSUPPORTED
logger = logging.getLogger('building')
binaryen_checked = False
EXPECTED_BINARYEN_VERSION = 124
_is_ar_cache: Dict[str, bool] = {}
user_requested_exports: Set[str] = set()
def get_building_env():
cache.ensure()
env = os.environ.copy()
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'
env['HOST_CXXFLAGS'] = '-W'
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')
return env
def llvm_backend_args():
args = ['-combiner-global-alias-analysis=false']
if not settings.DISABLE_EXCEPTION_CATCHING:
args += ['-enable-emscripten-cxx-exceptions']
if settings.EXCEPTION_CATCHING_ALLOWED:
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]
if settings.SUPPORT_LONGJMP == 'emscripten':
args += ['-enable-emscripten-sjlj']
elif settings.SUPPORT_LONGJMP == 'wasm':
args += ['-wasm-enable-sjlj']
if settings.WASM_EXCEPTIONS:
if settings.WASM_LEGACY_EXCEPTIONS:
args += ['-wasm-use-legacy-eh']
else:
args += ['-wasm-use-legacy-eh=0']
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, that we can pass to 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:
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')
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:
cmd.append('-u__cxa_atexit')
c_exports = [e for e in settings.EXPORTED_FUNCTIONS if is_c_symbol(e)]
c_exports = [demangle_c_symbol_name(e) for e in c_exports]
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.RELOCATABLE:
cmd.append('--experimental-pic')
cmd.append('--unresolved-symbols=import-dynamic')
if not settings.WASM_BIGINT:
cmd.append('--no-shlib-sigcheck')
if settings.SIDE_MODULE:
cmd.append('-shared')
else:
cmd.append('-pie')
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:
if not settings.EXPECT_MAIN:
cmd += ['--entry=_initialize']
else:
if settings.PROXY_TO_PTHREAD:
cmd += ['--entry=_emscripten_proxy_main']
else:
cmd += ['--no-entry']
if settings.STACK_FIRST:
cmd.append('--stack-first')
if not settings.RELOCATABLE:
cmd.append('--table-base=%s' % settings.TABLE_BASE)
if not settings.STACK_FIRST:
cmd.append('--global-base=%s' % settings.GLOBAL_BASE)
return cmd
def link_lld(args, target, external_symbols=None):
if not os.path.exists(WASM_LD):
exit_with_error('linker binary not found in LLVM directory: %s', WASM_LD)
args = [a for a in args if a not in ('--start-group', '--end-group')]
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')):
args.append('--keep-section=target_features')
cmd = [WASM_LD, '-o', target] + args
for a in llvm_backend_args():
cmd += ['-mllvm', a]
if settings.WASM_EXCEPTIONS:
cmd += ['-mllvm', '-wasm-enable-eh']
if settings.WASM_LEGACY_EXCEPTIONS:
cmd += ['-mllvm', '-wasm-use-legacy-eh']
else:
cmd += ['-mllvm', '-wasm-use-legacy-eh=0']
if settings.WASM_EXCEPTIONS or settings.SUPPORT_LONGJMP == 'wasm':
cmd += ['-mllvm', '-exception-model=wasm']
if settings.MEMORY64:
cmd.append('-mwasm64')
if '--relocatable' not in args and '-r' not in args:
cmd += lld_flags_for_executable(external_symbols)
cmd = get_command_with_possible_response_file(cmd)
check_call(cmd)
def get_command_with_possible_response_file(cmd):
force_response_files = os.getenv('EM_FORCE_RESPONSE_FILES')
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):
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 js_optimizer(filename, passes):
from . import js_optimizer
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') as f:
f.write('// EXTRA_INFO: ' + extra_info)
filename = temp
cmd = config.NODE_JS + [optimizer, filename] + passes
if not worker_js:
if settings.MAYBE_CLOSURE_COMPILER:
cmd += ['--closure-friendly']
if settings.EXPORT_ES6:
cmd += ['--export-es6']
if settings.VERBOSE:
cmd += ['--verbose']
if return_output:
shared.print_compiler_stage(cmd)
if shared.SKIP_SUBPROCS:
return ''
return check_call(cmd, stdout=PIPE).stdout
acorn_optimizer.counter += 1
basename = shared.unsuffixed(original_filename)
if '.jso' in basename:
basename = shared.unsuffixed(basename)
output_file = basename + '.jso%d.js' % acorn_optimizer.counter
shared.get_temp_files().note(output_file)
cmd += ['-o', output_file]
shared.print_compiler_stage(cmd)
if shared.SKIP_SUBPROCS:
return output_file
check_call(cmd)
save_intermediate(output_file, '%s.js' % passes[0])
return output_file
acorn_optimizer.counter = 0
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
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]
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():
if config.CLOSURE_COMPILER:
return config.CLOSURE_COMPILER
cmd = shared.get_npm_cmd('google-closure-compiler')
if not WINDOWS:
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):
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_IE_VERSION != UNSUPPORTED:
config['targets']['ie'] = str(settings.MIN_IE_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]
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 = [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.USE_WEBGPU:
CLOSURE_EXTERNS += [path_from_root('src/closure-externs/webgpu-externs.js')]
if settings.WASM_EXPORTS and not settings.DECLARE_ASM_MODULE_EXPORTS:
module_exports_suppressions = '\n'.join(['/**\n * @suppress {duplicate, undefinedVars}\n */\nvar %s;\n' % asmjs_mangle(i) for i in settings.WASM_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]
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
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.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']
args += ['--language_out', 'NO_TRANSPILE']
args += ['--emit_use_strict=false']
args += ['--assume_static_inheritance_is_not_used=false']
args += ['--charset=UTF8']
if settings.IGNORE_CLOSURE_COMPILER_ERRORS:
args.append('--jscomp_off=*')
for e in CLOSURE_EXTERNS:
args += ['--externs', e]
args += user_args
if settings.DEBUG_LEVEL > 1:
args += ['--debug']
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]
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
shutil.copyfile(filename, safe_filename)
return os.path.relpath(safe_filename, tempfiles.tmpdir)
for i in range(len(cmd)):
for prefix in ('--externs', '--js'):
if cmd[i] == prefix:
cmd[i + 1] = move_to_safe_7bit_ascii_filename(cmd[i + 1])
elif cmd[i].startswith(prefix + '='):
filename = cmd[i].split('=', 1)[1]
cmd[i] = '='.join([prefix, move_to_safe_7bit_ascii_filename(filename)])
outfile = tempfiles.get('.cc.js').name
cmd += ['--js_output_file', os.path.relpath(outfile, tempfiles.tmpdir)]
if not settings.MINIFY_WHITESPACE:
cmd += ['--formatting', 'PRETTY_PRINT']
if settings.WASM2JS:
cmd += ['--jscomp_off=checkTypes']
cmd += ['--jscomp_off=uselessCode']
shared.print_compiler_stage(cmd)
proc = run_process(cmd, stderr=PIPE, check=False, env=env, cwd=tempfiles.tmpdir, encoding='iso-8859-1' if WINDOWS else 'utf-8')
utils.delete_file(outfile + '.map')
closure_warnings = diagnostics.manager.warnings['closure']
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')
if DEBUG == 2 and (proc.returncode != 0 or (len(proc.stderr.strip()) > 0 and closure_warnings['enabled'])):
input_file = open(filename).read().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)
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']:
if closure_warnings['error']:
logger.error(proc.stderr)
else:
logger.warning(proc.stderr)
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
def minify_wasm_js(js_file, wasm_file, expensive_optimizations, debug_info):
passes = []
if not settings.LINKABLE:
passes.append('JSDCE' if not expensive_optimizations else 'AJSDCE')
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 not settings.LINKABLE:
if expensive_optimizations:
js_file = metadce(js_file,
wasm_file,
debug_info=debug_info,
last=not settings.MINIFY_WASM_IMPORTS_AND_EXPORTS)
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
def get_last_binaryen_opts():
return [f'--optimize-level={settings.OPT_LEVEL}',
f'--shrink-level={settings.SHRINK_LEVEL}',
'--optimize-stack-ir']
def metadce(js_file, wasm_file, debug_info, last):
logger.debug('running meta-DCE')
temp_files = shared.get_temp_files()
if settings.MAIN_MODULE:
exports = settings.WASM_EXPORTS
else:
exports = sorted(set(settings.WASM_EXPORTS) - set(settings.WASM_GLOBAL_EXPORTS))
extra_info = '{ "exports": [' + ','.join(f'["{asmjs_mangle(x)}", "{x}"]' for x in exports) + ']}'
txt = acorn_optimizer(js_file, ['emitDCEGraph', '--no-print'], return_output=True, extra_info=extra_info)
if shared.SKIP_SUBPROCS:
return js_file
graph = json.loads(txt)
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
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
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))
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)
unused_imports = []
unused_exports = []
PREFIX = 'unused: '
for line in out.splitlines():
if line.startswith(PREFIX):
name = line.replace(PREFIX, '').strip()
if settings.MAIN_MODULE and name.split('$')[-1] in ('wasmMemory', 'wasmTable'):
continue
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:
return js_file
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=json.dumps(extra_info))
def asyncify_lazy_load_code(wasm_target, debug):
args = ['--remove-memory-init', '--mod-asyncify-never-unwind']
if settings.OPT_LEVEL > 0:
args.append(opt_level_to_str(settings.OPT_LEVEL, settings.SHRINK_LEVEL))
run_wasm_opt(wasm_target,
wasm_target + '.lazy.wasm',
args=args,
debug=debug)
args = ['--mod-asyncify-always-and-only-unwind']
if settings.OPT_LEVEL > 0:
args.append(opt_level_to_str(settings.OPT_LEVEL, settings.SHRINK_LEVEL))
run_wasm_opt(infile=wasm_target,
outfile=wasm_target,
args=args,
debug=debug)
def minify_wasm_imports_and_exports(js_file, wasm_file, minify_exports, debug_info):
logger.debug('minifying wasm imports and exports')
args = []
if minify_exports:
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')
args += get_last_binaryen_opts()
out = run_wasm_opt(wasm_file, wasm_file, args, debug=debug_info, stdout=PIPE)
SEP = ' => '
mapping = {}
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
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=json.dumps(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)
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:
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 = js_optimizer(temp, passes)
wasm2js_js = utils.read_file(temp)
if use_closure_compiler == 2:
temp = shared.get_temp_files().get('.js').name
with open(temp, 'a') as f:
f.write(wasm2js_js)
temp = closure_compiler(temp, advanced=False)
wasm2js_js = utils.read_file(temp)
wasm2js_js = wasm2js_js.strip()
if wasm2js_js[-1] == ';':
wasm2js_js = wasm2js_js[:-1]
all_js = utils.read_file(js_file)
finds = re.findall(r'''[\w\d_$]+\[['"]__wasm2jsInstantiate__['"]\]''', all_js)
if not finds:
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)')
js_file = js_file + '.wasm2js.js'
utils.write_file(js_file, all_js)
return js_file
def strip(infile, outfile, debug=False, sections=None):
"""Strip DWARF and/or other specified sections from a wasm file"""
cmd = [LLVM_OBJCOPY, infile, outfile]
if debug:
cmd += ['--remove-section=.debug*']
if sections:
cmd += ['--remove-section=' + section for section in sections]
check_call(cmd)
def emit_debug_on_side(wasm_file, wasm_file_with_dwarf):
embedded_path = settings.SEPARATE_DWARF_URL
if not embedded_path:
embedded_path = os.path.relpath(wasm_file_with_dwarf,
os.path.dirname(wasm_file))
embedded_path = utils.normalize_path(embedded_path)
shutil.move(wasm_file, wasm_file_with_dwarf)
strip(wasm_file_with_dwarf, wasm_file, debug=True)
section_name = b'\x13external_debug_info'
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')
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 = {}
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:
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 a 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'!<arch>\n', b'!<thin>\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):
base_path = os.path.dirname(os.path.abspath(final_wasm))
sourcemap_cmd = [sys.executable, '-E', path_from_root('tools/wasm-sourcemap.py'),
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']
check_call(sourcemap_cmd)
def get_binaryen_feature_flags():
if settings.BINARYEN_FEATURES:
return settings.BINARYEN_FEATURES
else:
return ['--detect-features']
def check_binaryen(bindir):
opt = os.path.join(bindir, exe_suffix('wasm-opt'))
if not os.path.exists(opt):
exit_with_error('binaryen executable not found (%s). Please check your binaryen installation' % opt)
try:
output = run_process([opt, '--version'], stdout=PIPE).stdout
except subprocess.CalledProcessError:
exit_with_error('error running binaryen executable (%s). Please check your binaryen installation' % opt)
if output:
output = output.splitlines()[0]
try:
version = output.split()[2]
version = int(version)
except (IndexError, ValueError):
exit_with_error('error parsing binaryen version (%s). Please check your binaryen installation (%s)' % (output, opt))
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
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:
extra = ''
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']
cmd += get_binaryen_feature_flags()
if settings.GENERATE_SOURCE_MAP and outfile and tool in ['wasm-opt', 'wasm-emscripten-finalize']:
cmd += [f'--input-source-map={infile}.map']
cmd += [f'--output-source-map={outfile}.map']
shared.print_compiler_stage(cmd)
if shared.SKIP_SUBPROCS:
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):
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):
settings_json = json.dumps(settings.external_dict(), sort_keys=True, indent=2)
write_intermediate(settings_json, 'settings.json')
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.RELOCATABLE:
flags += ['--pass-arg=legalize-js-interface-export-originals']
if not settings.SIDE_MODULE:
flags += ['--pass-arg=legalize-js-interface-exported-helpers']
return flags
def get_emcc_node_flags(node_version):
if not node_version:
return []
str_node_version = "%02d%02d%02d" % node_version
return [f'-sMIN_NODE_VERSION={str_node_version}']