| |
| |
| |
| |
|
|
| 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') |
|
|
| |
| binaryen_checked = False |
| EXPECTED_BINARYEN_VERSION = 128 |
|
|
| _is_ar_cache: dict[str, bool] = {} |
| |
| user_requested_exports: set[str] = set() |
| |
| |
| |
| binaryen_features: list[str] = [] |
|
|
|
|
| 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] |
|
|
| match settings.SUPPORT_LONGJMP: |
| case 'emscripten': |
| |
| args += ['-enable-emscripten-sjlj'] |
| case 'wasm': |
| |
| 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'] |
|
|
| |
| |
| 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: |
| |
| |
| 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: |
| 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: |
| |
| |
| |
| |
| 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: |
| |
| 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') |
| 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): |
| |
| |
| |
| 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') |
|
|
| 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): |
| |
| 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] |
| |
| |
| 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): |
| |
| 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 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 |
| 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: |
| 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 |
|
|
| 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_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.AUDIO_WORKLET: |
| CLOSURE_EXTERNS += [path_from_root('src/closure-externs/audio-worklet-externs.js')] |
|
|
| |
| |
| |
| if settings.WASM_EXPORTS and not settings.DECLARE_ASM_MODULE_EXPORTS: |
| |
| 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] |
|
|
| |
| 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 = 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) |
|
|
| |
| 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() |
| 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: |
| |
| 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 = [] |
| for line in out.splitlines(): |
| if line.startswith('unused:'): |
| name = line.removeprefix('unused:').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=extra_info) |
|
|
|
|
| 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) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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']: |
| |
| 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 |
|
|
| |
| 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) |
| |
| 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 = run_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', encoding='utf-8') 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 += '.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) |
|
|
|
|
| |
| |
| 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_sections(wasm_file_with_dwarf, wasm_file, ['.debug*']) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| 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 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)) |
|
|
| |
| |
| 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'] |
|
|
| |
| rtn = wasm_sourcemap.main(sourcemap_cmd) |
| if rtn != 0: |
| exit_with_error('wasm-sourcemap failed (%s)', sourcemap_cmd) |
|
|
|
|
| def get_binaryen_feature_flags(): |
| |
| |
| 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') |
|
|
| |
| |
| 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', '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): |
| 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.SIDE_MODULE or settings.MAIN_MODULE: |
| |
| |
| |
| flags += ['--pass-arg=legalize-js-interface-export-originals'] |
| if not settings.SIDE_MODULE: |
| |
| |
| |
| flags += ['--pass-arg=legalize-js-interface-exported-helpers'] |
| return flags |
|
|