Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/tools/building.py
4128 views
1
# Copyright 2020 The Emscripten Authors. All rights reserved.
2
# Emscripten is available under two separate licenses, the MIT license and the
3
# University of Illinois/NCSA Open Source License. Both these licenses can be
4
# found in the LICENSE file.
5
6
from .toolchain_profiler import ToolchainProfiler
7
8
import json
9
import logging
10
import os
11
import re
12
import shlex
13
import shutil
14
import subprocess
15
import sys
16
from typing import Set, Dict
17
from subprocess import PIPE
18
19
from . import cache
20
from . import diagnostics
21
from . import response_file
22
from . import shared
23
from . import webassembly
24
from . import config
25
from . import utils
26
from .shared import CLANG_CC, CLANG_CXX
27
from .shared import LLVM_NM, EMCC, EMAR, EMXX, EMRANLIB, WASM_LD
28
from .shared import LLVM_OBJCOPY
29
from .shared import run_process, check_call, exit_with_error
30
from .shared import path_from_root
31
from .shared import asmjs_mangle, DEBUG
32
from .shared import LLVM_DWARFDUMP, demangle_c_symbol_name
33
from .shared import get_emscripten_temp_dir, exe_suffix, is_c_symbol
34
from .utils import WINDOWS
35
from .settings import settings
36
from .feature_matrix import UNSUPPORTED
37
38
logger = logging.getLogger('building')
39
40
# Building
41
binaryen_checked = False
42
EXPECTED_BINARYEN_VERSION = 124
43
44
_is_ar_cache: Dict[str, bool] = {}
45
# the exports the user requested
46
user_requested_exports: Set[str] = set()
47
48
49
def get_building_env():
50
cache.ensure()
51
env = os.environ.copy()
52
# point CC etc. to the em* tools.
53
env['CC'] = EMCC
54
env['CXX'] = EMXX
55
env['AR'] = EMAR
56
env['LD'] = EMCC
57
env['NM'] = LLVM_NM
58
env['LDSHARED'] = EMCC
59
env['RANLIB'] = EMRANLIB
60
env['EMSCRIPTEN_TOOLS'] = path_from_root('tools')
61
env['HOST_CC'] = CLANG_CC
62
env['HOST_CXX'] = CLANG_CXX
63
env['HOST_CFLAGS'] = '-W' # if set to nothing, CFLAGS is used, which we don't want
64
env['HOST_CXXFLAGS'] = '-W' # if set to nothing, CXXFLAGS is used, which we don't want
65
env['PKG_CONFIG_LIBDIR'] = cache.get_sysroot_dir('local/lib/pkgconfig') + os.path.pathsep + cache.get_sysroot_dir('lib/pkgconfig')
66
env['PKG_CONFIG_PATH'] = os.environ.get('EM_PKG_CONFIG_PATH', '')
67
env['EMSCRIPTEN'] = path_from_root()
68
env['PATH'] = cache.get_sysroot_dir('bin') + os.pathsep + env['PATH']
69
env['ACLOCAL_PATH'] = cache.get_sysroot_dir('share/aclocal')
70
env['CROSS_COMPILE'] = path_from_root('em') # produces /path/to/emscripten/em , which then can have 'cc', 'ar', etc appended to it
71
return env
72
73
74
def llvm_backend_args():
75
# disable slow and relatively unimportant optimization passes
76
args = ['-combiner-global-alias-analysis=false']
77
78
# asm.js-style exception handling
79
if not settings.DISABLE_EXCEPTION_CATCHING:
80
args += ['-enable-emscripten-cxx-exceptions']
81
if settings.EXCEPTION_CATCHING_ALLOWED:
82
# When 'main' has a non-standard signature, LLVM outlines its content out to
83
# '__original_main'. So we add it to the allowed list as well.
84
if 'main' in settings.EXCEPTION_CATCHING_ALLOWED:
85
settings.EXCEPTION_CATCHING_ALLOWED += ['__original_main', '__main_argc_argv']
86
allowed = ','.join(settings.EXCEPTION_CATCHING_ALLOWED)
87
args += ['-emscripten-cxx-exceptions-allowed=' + allowed]
88
89
# asm.js-style setjmp/longjmp handling
90
if settings.SUPPORT_LONGJMP == 'emscripten':
91
args += ['-enable-emscripten-sjlj']
92
# setjmp/longjmp handling using Wasm EH
93
elif settings.SUPPORT_LONGJMP == 'wasm':
94
args += ['-wasm-enable-sjlj']
95
96
if settings.WASM_EXCEPTIONS:
97
if settings.WASM_LEGACY_EXCEPTIONS:
98
args += ['-wasm-use-legacy-eh']
99
else:
100
args += ['-wasm-use-legacy-eh=0']
101
102
# better (smaller, sometimes faster) codegen, see binaryen#1054
103
# and https://bugs.llvm.org/show_bug.cgi?id=39488
104
args += ['-disable-lsr']
105
106
return args
107
108
109
@ToolchainProfiler.profile()
110
def link_to_object(args, target):
111
link_lld(args + ['--relocatable'], target)
112
113
114
def side_module_external_deps(external_symbols):
115
"""Find the list of the external symbols that are needed by the
116
linked side modules.
117
"""
118
deps = set()
119
for sym in settings.SIDE_MODULE_IMPORTS:
120
sym = demangle_c_symbol_name(sym)
121
if sym in external_symbols:
122
deps = deps.union(external_symbols[sym])
123
return sorted(deps)
124
125
126
def create_stub_object(external_symbols):
127
"""Create a stub object, based on the JS library symbols and their
128
dependencies, that we can pass to wasm-ld.
129
"""
130
stubfile = shared.get_temp_files().get('libemscripten_js_symbols.so').name
131
stubs = ['#STUB']
132
for name, deps in external_symbols.items():
133
if not name.startswith('$'):
134
stubs.append('%s: %s' % (name, ','.join(deps)))
135
utils.write_file(stubfile, '\n'.join(stubs))
136
return stubfile
137
138
139
def lld_flags_for_executable(external_symbols):
140
cmd = []
141
if external_symbols:
142
if settings.INCLUDE_FULL_LIBRARY:
143
# When INCLUDE_FULL_LIBRARY is set try to export every possible
144
# native dependency of a JS function.
145
all_deps = set()
146
for deps in external_symbols.values():
147
for dep in deps:
148
if dep not in all_deps:
149
cmd.append('--export-if-defined=' + dep)
150
all_deps.add(dep)
151
stub = create_stub_object(external_symbols)
152
cmd.append(stub)
153
154
if not settings.ERROR_ON_UNDEFINED_SYMBOLS:
155
cmd.append('--import-undefined')
156
157
if settings.IMPORTED_MEMORY:
158
cmd.append('--import-memory')
159
160
if settings.SHARED_MEMORY:
161
cmd.append('--shared-memory')
162
163
# wasm-ld can strip debug info for us. this strips both the Names
164
# section and DWARF, so we can only use it when we don't need any of
165
# those things.
166
if (not settings.GENERATE_DWARF and
167
not settings.EMIT_SYMBOL_MAP and
168
not settings.GENERATE_SOURCE_MAP and
169
not settings.EMIT_NAME_SECTION and
170
not settings.ASYNCIFY):
171
cmd.append('--strip-debug')
172
173
if settings.LINKABLE:
174
cmd.append('--export-dynamic')
175
176
if settings.LTO and not settings.EXIT_RUNTIME:
177
# The WebAssembly backend can generate new references to `__cxa_atexit` at
178
# LTO time. This `-u` flag forces the `__cxa_atexit` symbol to be
179
# included at LTO time. For other such symbols we exclude them from LTO
180
# and always build them as normal object files, but that would inhibit the
181
# LowerGlobalDtors optimization which allows destructors to be completely
182
# removed when __cxa_atexit is a no-op.
183
cmd.append('-u__cxa_atexit')
184
185
c_exports = [e for e in settings.EXPORTED_FUNCTIONS if is_c_symbol(e)]
186
# Strip the leading underscores
187
c_exports = [demangle_c_symbol_name(e) for e in c_exports]
188
# Filter out symbols external/JS symbols
189
c_exports = [e for e in c_exports if e not in external_symbols]
190
c_exports += settings.REQUIRED_EXPORTS
191
if settings.MAIN_MODULE:
192
c_exports += side_module_external_deps(external_symbols)
193
for export in c_exports:
194
if settings.ERROR_ON_UNDEFINED_SYMBOLS:
195
cmd.append(f'--export={export}')
196
else:
197
cmd.append(f'--export-if-defined={export}')
198
199
cmd.extend(f'--export-if-defined={e}' for e in settings.EXPORT_IF_DEFINED)
200
201
if settings.RELOCATABLE:
202
cmd.append('--experimental-pic')
203
cmd.append('--unresolved-symbols=import-dynamic')
204
if not settings.WASM_BIGINT:
205
# When we don't have WASM_BIGINT available, JS signature legalization
206
# in binaryen will mutate the signatures of the imports/exports of our
207
# shared libraries. Because of this we need to disabled signature
208
# checking of shared library functions in this case.
209
cmd.append('--no-shlib-sigcheck')
210
if settings.SIDE_MODULE:
211
cmd.append('-shared')
212
else:
213
cmd.append('-pie')
214
if not settings.LINKABLE:
215
cmd.append('--no-export-dynamic')
216
else:
217
cmd.append('--export-table')
218
if settings.ALLOW_TABLE_GROWTH:
219
cmd.append('--growable-table')
220
221
if not settings.SIDE_MODULE:
222
cmd += ['-z', 'stack-size=%s' % settings.STACK_SIZE]
223
224
if settings.ALLOW_MEMORY_GROWTH:
225
cmd += ['--max-memory=%d' % settings.MAXIMUM_MEMORY]
226
else:
227
cmd += ['--no-growable-memory']
228
229
if settings.INITIAL_HEAP != -1:
230
cmd += ['--initial-heap=%d' % settings.INITIAL_HEAP]
231
if settings.INITIAL_MEMORY != -1:
232
cmd += ['--initial-memory=%d' % settings.INITIAL_MEMORY]
233
234
if settings.STANDALONE_WASM:
235
# when settings.EXPECT_MAIN is set we fall back to wasm-ld default of _start
236
if not settings.EXPECT_MAIN:
237
cmd += ['--entry=_initialize']
238
else:
239
if settings.PROXY_TO_PTHREAD:
240
cmd += ['--entry=_emscripten_proxy_main']
241
else:
242
# TODO(sbc): Avoid passing --no-entry when we know we have an entry point.
243
# For now we need to do this since the entry point can be either `main` or
244
# `__main_argv_argc`, but we should address that by using a single `_start`
245
# function like we do in STANDALONE_WASM mode.
246
cmd += ['--no-entry']
247
248
if settings.STACK_FIRST:
249
cmd.append('--stack-first')
250
251
if not settings.RELOCATABLE:
252
cmd.append('--table-base=%s' % settings.TABLE_BASE)
253
if not settings.STACK_FIRST:
254
cmd.append('--global-base=%s' % settings.GLOBAL_BASE)
255
256
return cmd
257
258
259
def link_lld(args, target, external_symbols=None):
260
if not os.path.exists(WASM_LD):
261
exit_with_error('linker binary not found in LLVM directory: %s', WASM_LD)
262
# runs lld to link things.
263
# lld doesn't currently support --start-group/--end-group since the
264
# semantics are more like the windows linker where there is no need for
265
# grouping.
266
args = [a for a in args if a not in ('--start-group', '--end-group')]
267
268
# Emscripten currently expects linkable output (SIDE_MODULE/MAIN_MODULE) to
269
# include all archive contents.
270
if settings.LINKABLE:
271
args.insert(0, '--whole-archive')
272
args.append('--no-whole-archive')
273
274
if settings.STRICT and '--no-fatal-warnings' not in args:
275
args.append('--fatal-warnings')
276
277
if any(a in args for a in ('--strip-all', '-s')):
278
# Tell wasm-ld to always generate a target_features section even if --strip-all/-s
279
# is passed.
280
args.append('--keep-section=target_features')
281
282
cmd = [WASM_LD, '-o', target] + args
283
for a in llvm_backend_args():
284
cmd += ['-mllvm', a]
285
286
if settings.WASM_EXCEPTIONS:
287
cmd += ['-mllvm', '-wasm-enable-eh']
288
if settings.WASM_LEGACY_EXCEPTIONS:
289
cmd += ['-mllvm', '-wasm-use-legacy-eh']
290
else:
291
cmd += ['-mllvm', '-wasm-use-legacy-eh=0']
292
if settings.WASM_EXCEPTIONS or settings.SUPPORT_LONGJMP == 'wasm':
293
cmd += ['-mllvm', '-exception-model=wasm']
294
295
if settings.MEMORY64:
296
cmd.append('-mwasm64')
297
298
# For relocatable output (generating an object file) we don't pass any of the
299
# normal linker flags that are used when building and executable
300
if '--relocatable' not in args and '-r' not in args:
301
cmd += lld_flags_for_executable(external_symbols)
302
303
cmd = get_command_with_possible_response_file(cmd)
304
check_call(cmd)
305
306
307
def get_command_with_possible_response_file(cmd):
308
# One of None, 0 or 1. (None: do default decision, 0: force disable, 1: force enable)
309
force_response_files = os.getenv('EM_FORCE_RESPONSE_FILES')
310
311
# Different OS have different limits. The most limiting usually is Windows one
312
# which is set at 8191 characters. We could just use that, but it leads to
313
# problems when invoking shell wrappers (e.g. emcc.bat), which, in turn,
314
# pass arguments to some longer command like `(full path to Clang) ...args`.
315
# In that scenario, even if the initial command line is short enough, the
316
# subprocess can still run into the Command Line Too Long error.
317
# Reduce the limit by ~1K for now to be on the safe side, but we might need to
318
# adjust this in the future if it turns out not to be enough.
319
if (len(shlex.join(cmd)) <= 7000 and force_response_files != '1') or force_response_files == '0':
320
return cmd
321
322
logger.debug('using response file for %s' % cmd[0])
323
filename = response_file.create_response_file(cmd[1:], shared.TEMP_DIR)
324
new_cmd = [cmd[0], "@" + filename]
325
return new_cmd
326
327
328
def emar(action, output_filename, filenames, stdout=None, stderr=None, env=None):
329
utils.delete_file(output_filename)
330
cmd = [EMAR, action, output_filename] + filenames
331
cmd = get_command_with_possible_response_file(cmd)
332
run_process(cmd, stdout=stdout, stderr=stderr, env=env)
333
334
if 'c' in action:
335
assert os.path.exists(output_filename), 'emar could not create output file: ' + output_filename
336
337
338
def opt_level_to_str(opt_level, shrink_level=0):
339
# convert opt_level/shrink_level pair to a string argument like -O1
340
if opt_level == 0:
341
return '-O0'
342
if shrink_level == 1:
343
return '-Os'
344
elif shrink_level >= 2:
345
return '-Oz'
346
else:
347
return f'-O{min(opt_level, 3)}'
348
349
350
def js_optimizer(filename, passes):
351
from . import js_optimizer
352
try:
353
return js_optimizer.run_on_file(filename, passes)
354
except subprocess.CalledProcessError as e:
355
exit_with_error("'%s' failed (%d)", ' '.join(e.cmd), e.returncode)
356
357
358
# run JS optimizer on some JS, ignoring asm.js contents if any - just run on it all
359
def acorn_optimizer(filename, passes, extra_info=None, return_output=False, worker_js=False):
360
optimizer = path_from_root('tools/acorn-optimizer.mjs')
361
original_filename = filename
362
if extra_info is not None and not shared.SKIP_SUBPROCS:
363
temp_files = shared.get_temp_files()
364
temp = temp_files.get('.js', prefix='emcc_acorn_info_').name
365
shutil.copyfile(filename, temp)
366
with open(temp, 'a') as f:
367
f.write('// EXTRA_INFO: ' + extra_info)
368
filename = temp
369
cmd = config.NODE_JS + [optimizer, filename] + passes
370
if not worker_js:
371
# Keep JS code comments intact through the acorn optimization pass so that
372
# JSDoc comments will be carried over to a later Closure run.
373
if settings.MAYBE_CLOSURE_COMPILER:
374
cmd += ['--closure-friendly']
375
if settings.EXPORT_ES6:
376
cmd += ['--export-es6']
377
if settings.VERBOSE:
378
cmd += ['--verbose']
379
if return_output:
380
shared.print_compiler_stage(cmd)
381
if shared.SKIP_SUBPROCS:
382
return ''
383
return check_call(cmd, stdout=PIPE).stdout
384
385
acorn_optimizer.counter += 1
386
basename = shared.unsuffixed(original_filename)
387
if '.jso' in basename:
388
basename = shared.unsuffixed(basename)
389
output_file = basename + '.jso%d.js' % acorn_optimizer.counter
390
shared.get_temp_files().note(output_file)
391
cmd += ['-o', output_file]
392
shared.print_compiler_stage(cmd)
393
if shared.SKIP_SUBPROCS:
394
return output_file
395
check_call(cmd)
396
save_intermediate(output_file, '%s.js' % passes[0])
397
return output_file
398
399
400
acorn_optimizer.counter = 0 # type: ignore
401
402
WASM_CALL_CTORS = '__wasm_call_ctors'
403
404
405
# evals ctors. if binaryen_bin is provided, it is the dir of the binaryen tool
406
# for this, and we are in wasm mode
407
def eval_ctors(js_file, wasm_file, debug_info):
408
CTOR_ADD_PATTERN = f"wasmExports['{WASM_CALL_CTORS}']();"
409
410
js = utils.read_file(js_file)
411
412
has_wasm_call_ctors = False
413
414
# eval the ctor caller as well as main, or, in standalone mode, the proper
415
# entry/init function
416
if not settings.STANDALONE_WASM:
417
ctors = []
418
kept_ctors = []
419
has_wasm_call_ctors = CTOR_ADD_PATTERN in js
420
if has_wasm_call_ctors:
421
ctors += [WASM_CALL_CTORS]
422
if settings.HAS_MAIN:
423
main = 'main'
424
if '__main_argc_argv' in settings.WASM_EXPORTS:
425
main = '__main_argc_argv'
426
ctors += [main]
427
# TODO perhaps remove the call to main from the JS? or is this an abi
428
# we want to preserve?
429
kept_ctors += [main]
430
if not ctors:
431
logger.info('ctor_evaller: no ctors')
432
return
433
args = ['--ctors=' + ','.join(ctors)]
434
if kept_ctors:
435
args += ['--kept-exports=' + ','.join(kept_ctors)]
436
else:
437
if settings.EXPECT_MAIN:
438
ctor = '_start'
439
else:
440
ctor = '_initialize'
441
args = ['--ctors=' + ctor, '--kept-exports=' + ctor]
442
if settings.EVAL_CTORS == 2:
443
args += ['--ignore-external-input']
444
logger.info('ctor_evaller: trying to eval global ctors (' + ' '.join(args) + ')')
445
out = run_binaryen_command('wasm-ctor-eval', wasm_file, wasm_file, args=args, stdout=PIPE, debug=debug_info)
446
logger.info('\n\n' + out)
447
num_successful = out.count('success on')
448
if num_successful and has_wasm_call_ctors:
449
js = js.replace(CTOR_ADD_PATTERN, '')
450
settings.WASM_EXPORTS.remove(WASM_CALL_CTORS)
451
utils.write_file(js_file, js)
452
453
454
def get_closure_compiler():
455
# First check if the user configured a specific CLOSURE_COMPILER in their settings
456
if config.CLOSURE_COMPILER:
457
return config.CLOSURE_COMPILER
458
459
# Otherwise use the one installed via npm
460
cmd = shared.get_npm_cmd('google-closure-compiler')
461
if not WINDOWS:
462
# Work around an issue that Closure compiler can take up a lot of memory and crash in an error
463
# "FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap
464
# out of memory"
465
cmd.insert(-1, '--max_old_space_size=8192')
466
return cmd
467
468
469
def check_closure_compiler(cmd, args, env, allowed_to_fail):
470
cmd = cmd + args + ['--version']
471
try:
472
output = run_process(cmd, stdout=PIPE, env=env).stdout
473
except Exception as e:
474
if allowed_to_fail:
475
return False
476
if isinstance(e, subprocess.CalledProcessError):
477
sys.stderr.write(e.stdout)
478
sys.stderr.write(str(e) + '\n')
479
exit_with_error('closure compiler (%s) did not execute properly!' % shlex.join(cmd))
480
481
if 'Version:' not in output:
482
if allowed_to_fail:
483
return False
484
exit_with_error('unrecognized closure compiler --version output (%s):\n%s' % (shlex.join(cmd), output))
485
486
return True
487
488
489
def get_closure_compiler_and_env(user_args):
490
env = shared.env_with_node_in_path()
491
closure_cmd = get_closure_compiler()
492
493
native_closure_compiler_works = check_closure_compiler(closure_cmd, user_args, env, allowed_to_fail=True)
494
if not native_closure_compiler_works and not any(a.startswith('--platform') for a in user_args):
495
# Run with Java Closure compiler as a fallback if the native version does not work.
496
# This can happen, for example, on arm64 macOS machines that do not have Rosetta installed.
497
logger.warning('falling back to java version of closure compiler')
498
user_args.append('--platform=java')
499
check_closure_compiler(closure_cmd, user_args, env, allowed_to_fail=False)
500
501
return closure_cmd, env
502
503
504
def version_split(v):
505
"""Split version setting number (e.g. 162000) into versions string (e.g. "16.2.0")
506
"""
507
v = str(v).rjust(6, '0')
508
assert len(v) == 6
509
m = re.match(r'(\d{2})(\d{2})(\d{2})', v)
510
major, minor, rev = m.group(1, 2, 3)
511
return f'{int(major)}.{int(minor)}.{int(rev)}'
512
513
514
@ToolchainProfiler.profile()
515
def transpile(filename):
516
config = {
517
'sourceType': 'script',
518
'presets': ['@babel/preset-env'],
519
'targets': {},
520
}
521
if settings.MIN_CHROME_VERSION != UNSUPPORTED:
522
config['targets']['chrome'] = str(settings.MIN_CHROME_VERSION)
523
if settings.MIN_FIREFOX_VERSION != UNSUPPORTED:
524
config['targets']['firefox'] = str(settings.MIN_FIREFOX_VERSION)
525
if settings.MIN_IE_VERSION != UNSUPPORTED:
526
config['targets']['ie'] = str(settings.MIN_IE_VERSION)
527
if settings.MIN_SAFARI_VERSION != UNSUPPORTED:
528
config['targets']['safari'] = version_split(settings.MIN_SAFARI_VERSION)
529
if settings.MIN_NODE_VERSION != UNSUPPORTED:
530
config['targets']['node'] = version_split(settings.MIN_NODE_VERSION)
531
config_json = json.dumps(config, indent=2)
532
outfile = shared.get_temp_files().get('babel.js').name
533
config_file = shared.get_temp_files().get('babel_config.json').name
534
logger.debug(config_json)
535
utils.write_file(config_file, config_json)
536
cmd = shared.get_npm_cmd('babel') + [filename, '-o', outfile, '--config-file', config_file]
537
# Babel needs access to `node_modules` for things like `preset-env`, but the
538
# location of the config file (and the current working directory) might not be
539
# in the emscripten tree, so we explicitly set NODE_PATH here.
540
env = shared.env_with_node_in_path()
541
env['NODE_PATH'] = path_from_root('node_modules')
542
check_call(cmd, env=env)
543
return outfile
544
545
546
@ToolchainProfiler.profile()
547
def closure_compiler(filename, advanced=True, extra_closure_args=None):
548
user_args = []
549
env_args = os.environ.get('EMCC_CLOSURE_ARGS')
550
if env_args:
551
user_args += shlex.split(env_args)
552
if extra_closure_args:
553
user_args += extra_closure_args
554
555
closure_cmd, env = get_closure_compiler_and_env(user_args)
556
557
# Closure externs file contains known symbols to be extern to the minification, Closure
558
# should not minify these symbol names.
559
CLOSURE_EXTERNS = [path_from_root('src/closure-externs/closure-externs.js')]
560
561
if settings.MODULARIZE and settings.ENVIRONMENT_MAY_BE_WEB and not settings.EXPORT_ES6:
562
CLOSURE_EXTERNS += [path_from_root('src/closure-externs/modularize-externs.js')]
563
564
if settings.USE_WEBGPU:
565
CLOSURE_EXTERNS += [path_from_root('src/closure-externs/webgpu-externs.js')]
566
567
# Closure compiler needs to know about all exports that come from the wasm module, because to optimize for small code size,
568
# the exported symbols are added to global scope via a foreach loop in a way that evades Closure's static analysis. With an explicit
569
# externs file for the exports, Closure is able to reason about the exports.
570
if settings.WASM_EXPORTS and not settings.DECLARE_ASM_MODULE_EXPORTS:
571
# Generate an exports file that records all the exported symbols from the wasm module.
572
module_exports_suppressions = '\n'.join(['/**\n * @suppress {duplicate, undefinedVars}\n */\nvar %s;\n' % asmjs_mangle(i) for i in settings.WASM_EXPORTS])
573
exports_file = shared.get_temp_files().get('.js', prefix='emcc_module_exports_')
574
exports_file.write(module_exports_suppressions.encode())
575
exports_file.close()
576
577
CLOSURE_EXTERNS += [exports_file.name]
578
579
# Node.js specific externs
580
if settings.ENVIRONMENT_MAY_BE_NODE:
581
NODE_EXTERNS_BASE = path_from_root('third_party/closure-compiler/node-externs')
582
NODE_EXTERNS = os.listdir(NODE_EXTERNS_BASE)
583
NODE_EXTERNS = [os.path.join(NODE_EXTERNS_BASE, name) for name in NODE_EXTERNS
584
if name.endswith('.js')]
585
CLOSURE_EXTERNS += [path_from_root('src/closure-externs/node-externs.js')] + NODE_EXTERNS
586
587
# V8/SpiderMonkey shell specific externs
588
if settings.ENVIRONMENT_MAY_BE_SHELL:
589
V8_EXTERNS = [path_from_root('src/closure-externs/v8-externs.js')]
590
SPIDERMONKEY_EXTERNS = [path_from_root('src/closure-externs/spidermonkey-externs.js')]
591
CLOSURE_EXTERNS += V8_EXTERNS + SPIDERMONKEY_EXTERNS
592
593
# Web environment specific externs
594
if settings.ENVIRONMENT_MAY_BE_WEB or settings.ENVIRONMENT_MAY_BE_WORKER:
595
BROWSER_EXTERNS_BASE = path_from_root('src/closure-externs/browser-externs')
596
if os.path.isdir(BROWSER_EXTERNS_BASE):
597
BROWSER_EXTERNS = os.listdir(BROWSER_EXTERNS_BASE)
598
BROWSER_EXTERNS = [os.path.join(BROWSER_EXTERNS_BASE, name) for name in BROWSER_EXTERNS
599
if name.endswith('.js')]
600
CLOSURE_EXTERNS += BROWSER_EXTERNS
601
602
if settings.DYNCALLS:
603
CLOSURE_EXTERNS += [path_from_root('src/closure-externs/dyncall-externs.js')]
604
605
args = ['--compilation_level', 'ADVANCED_OPTIMIZATIONS' if advanced else 'SIMPLE_OPTIMIZATIONS']
606
args += ['--language_in', 'UNSTABLE']
607
# We do transpilation using babel
608
args += ['--language_out', 'NO_TRANSPILE']
609
# Tell closure never to inject the 'use strict' directive.
610
args += ['--emit_use_strict=false']
611
args += ['--assume_static_inheritance_is_not_used=false']
612
# Always output UTF-8 files, this helps generate UTF-8 code points instead of escaping code points with \uxxxx inside strings.
613
# Closure outputs ASCII by default, and must be adjusted to output UTF8 (https://github.com/google/closure-compiler/issues/4158)
614
args += ['--charset=UTF8']
615
616
if settings.IGNORE_CLOSURE_COMPILER_ERRORS:
617
args.append('--jscomp_off=*')
618
# Specify input file relative to the temp directory to avoid specifying non-7-bit-ASCII path names.
619
for e in CLOSURE_EXTERNS:
620
args += ['--externs', e]
621
args += user_args
622
623
if settings.DEBUG_LEVEL > 1:
624
args += ['--debug']
625
626
# Now that we have run closure compiler once, we have stripped all the closure compiler
627
# annotations from the source code and we no longer need to worry about generating closure
628
# friendly code.
629
# This means all the calls to acorn_optimizer that come after this will now run without
630
# --closure-friendly
631
settings.MAYBE_CLOSURE_COMPILER = False
632
633
cmd = closure_cmd + args
634
return run_closure_cmd(cmd, filename, env)
635
636
637
def run_closure_cmd(cmd, filename, env):
638
cmd += ['--js', filename]
639
640
# Closure compiler is unable to deal with path names that are not 7-bit ASCII:
641
# https://github.com/google/closure-compiler/issues/3784
642
tempfiles = shared.get_temp_files()
643
644
def move_to_safe_7bit_ascii_filename(filename):
645
if os.path.abspath(filename).isascii():
646
return os.path.abspath(filename)
647
safe_filename = tempfiles.get('.js').name # Safe 7-bit filename
648
shutil.copyfile(filename, safe_filename)
649
return os.path.relpath(safe_filename, tempfiles.tmpdir)
650
651
for i in range(len(cmd)):
652
for prefix in ('--externs', '--js'):
653
# Handle the case where the flag and the value are two separate arguments.
654
if cmd[i] == prefix:
655
cmd[i + 1] = move_to_safe_7bit_ascii_filename(cmd[i + 1])
656
# and the case where they are one argument, e.g. --externs=foo.js
657
elif cmd[i].startswith(prefix + '='):
658
# Replace the argument with a version that has a safe filename.
659
filename = cmd[i].split('=', 1)[1]
660
cmd[i] = '='.join([prefix, move_to_safe_7bit_ascii_filename(filename)])
661
662
outfile = tempfiles.get('.cc.js').name # Safe 7-bit filename
663
664
# Specify output file relative to the temp directory to avoid specifying non-7-bit-ASCII path names.
665
cmd += ['--js_output_file', os.path.relpath(outfile, tempfiles.tmpdir)]
666
if not settings.MINIFY_WHITESPACE:
667
cmd += ['--formatting', 'PRETTY_PRINT']
668
669
if settings.WASM2JS:
670
# In WASM2JS mode, the WebAssembly object is polyfilled, which triggers
671
# Closure's built-in type check:
672
# externs.zip//webassembly.js:29:18: WARNING - [JSC_TYPE_MISMATCH] initializing variable
673
# We cannot fix this warning externally, since adding /** @suppress{checkTypes} */
674
# to the polyfill is "in the wrong end". So mute this warning globally to
675
# allow clean Closure output. https://github.com/google/closure-compiler/issues/4108
676
cmd += ['--jscomp_off=checkTypes']
677
678
# WASM2JS codegen routinely generates expressions that are unused, e.g.
679
# WARNING - [JSC_USELESS_CODE] Suspicious code. The result of the 'bitor' operator is not being used.
680
# s(0) | 0;
681
# ^^^^^^^^
682
# Turn off this check in Closure to allow clean Closure output.
683
cmd += ['--jscomp_off=uselessCode']
684
685
shared.print_compiler_stage(cmd)
686
687
# Closure compiler does not work if any of the input files contain characters outside the
688
# 7-bit ASCII range. Therefore make sure the command line we pass does not contain any such
689
# input files by passing all input filenames relative to the cwd. (user temp directory might
690
# be in user's home directory, and user's profile name might contain unicode characters)
691
# https://github.com/google/closure-compiler/issues/4159: Closure outputs stdout/stderr in iso-8859-1 on Windows.
692
proc = run_process(cmd, stderr=PIPE, check=False, env=env, cwd=tempfiles.tmpdir, encoding='iso-8859-1' if WINDOWS else 'utf-8')
693
694
# XXX Closure bug: if Closure is invoked with --create_source_map, Closure should create a
695
# outfile.map source map file (https://github.com/google/closure-compiler/wiki/Source-Maps)
696
# But it looks like it creates such files on Linux(?) even without setting that command line
697
# flag (and currently we don't), so delete the produced source map file to not leak files in
698
# temp directory.
699
utils.delete_file(outfile + '.map')
700
701
closure_warnings = diagnostics.manager.warnings['closure']
702
703
# Print Closure diagnostics result up front.
704
if proc.returncode != 0:
705
logger.error('Closure compiler run failed:\n')
706
elif len(proc.stderr.strip()) > 0 and closure_warnings['enabled']:
707
if closure_warnings['error']:
708
logger.error('Closure compiler completed with warnings and -Werror=closure enabled, aborting!\n')
709
else:
710
logger.warning('Closure compiler completed with warnings:\n')
711
712
# Print input file (long wall of text!)
713
if DEBUG == 2 and (proc.returncode != 0 or (len(proc.stderr.strip()) > 0 and closure_warnings['enabled'])):
714
input_file = open(filename).read().splitlines()
715
for i in range(len(input_file)):
716
sys.stderr.write(f'{i + 1}: {input_file[i]}\n')
717
718
if proc.returncode != 0:
719
logger.error(proc.stderr) # print list of errors (possibly long wall of text if input was minified)
720
721
# Exit and print final hint to get clearer output
722
msg = f'closure compiler failed (rc: {proc.returncode}): {shlex.join(cmd)}'
723
if settings.MINIFY_WHITESPACE:
724
msg += ' the error message may be clearer with -g1 and EMCC_DEBUG=2 set'
725
exit_with_error(msg)
726
727
if len(proc.stderr.strip()) > 0 and closure_warnings['enabled']:
728
# print list of warnings (possibly long wall of text if input was minified)
729
if closure_warnings['error']:
730
logger.error(proc.stderr)
731
else:
732
logger.warning(proc.stderr)
733
734
# Exit and/or print final hint to get clearer output
735
if settings.MINIFY_WHITESPACE:
736
logger.warning('(rerun with -g1 linker flag for an unminified output)')
737
elif DEBUG != 2:
738
logger.warning('(rerun with EMCC_DEBUG=2 enabled to dump Closure input file)')
739
740
if closure_warnings['error']:
741
exit_with_error('closure compiler produced warnings and -W=error=closure enabled')
742
743
return outfile
744
745
746
# minify the final wasm+JS combination. this is done after all the JS
747
# and wasm optimizations; here we do the very final optimizations on them
748
def minify_wasm_js(js_file, wasm_file, expensive_optimizations, debug_info):
749
# start with JSDCE, to clean up obvious JS garbage. When optimizing for size,
750
# use AJSDCE (aggressive JS DCE, performs multiple iterations). Clean up
751
# whitespace if necessary too.
752
passes = []
753
if not settings.LINKABLE:
754
passes.append('JSDCE' if not expensive_optimizations else 'AJSDCE')
755
# Don't minify if we are going to run closure compiler afterwards
756
minify = settings.MINIFY_WHITESPACE and not settings.MAYBE_CLOSURE_COMPILER
757
if minify:
758
passes.append('--minify-whitespace')
759
if passes:
760
logger.debug('running cleanup on shell code: ' + ' '.join(passes))
761
js_file = acorn_optimizer(js_file, passes)
762
# if we can optimize this js+wasm combination under the assumption no one else
763
# will see the internals, do so
764
if not settings.LINKABLE:
765
# if we are optimizing for size, shrink the combined wasm+JS
766
# TODO: support this when a symbol map is used
767
if expensive_optimizations:
768
js_file = metadce(js_file,
769
wasm_file,
770
debug_info=debug_info,
771
last=not settings.MINIFY_WASM_IMPORTS_AND_EXPORTS)
772
# now that we removed unneeded communication between js and wasm, we can clean up
773
# the js some more.
774
passes = ['AJSDCE']
775
if minify:
776
passes.append('--minify-whitespace')
777
logger.debug('running post-meta-DCE cleanup on shell code: ' + ' '.join(passes))
778
js_file = acorn_optimizer(js_file, passes)
779
if settings.MINIFY_WASM_IMPORTS_AND_EXPORTS:
780
js_file = minify_wasm_imports_and_exports(js_file, wasm_file,
781
minify_exports=settings.MINIFY_WASM_EXPORT_NAMES,
782
debug_info=debug_info)
783
return js_file
784
785
786
# get the flags to pass into the very last binaryen tool invocation, that runs
787
# the final set of optimizations
788
def get_last_binaryen_opts():
789
return [f'--optimize-level={settings.OPT_LEVEL}',
790
f'--shrink-level={settings.SHRINK_LEVEL}',
791
'--optimize-stack-ir']
792
793
794
# run binaryen's wasm-metadce to dce both js and wasm
795
def metadce(js_file, wasm_file, debug_info, last):
796
logger.debug('running meta-DCE')
797
temp_files = shared.get_temp_files()
798
# first, get the JS part of the graph
799
if settings.MAIN_MODULE:
800
# For the main module we include all exports as possible roots, not just function exports.
801
# This means that any usages of data symbols within the JS or in the side modules can/will keep
802
# these exports alive on the wasm module.
803
# This is important today for weak data symbols that are defined by the main and the side module
804
# (i.e. RTTI info). We want to make sure the main module's symbols get added to wasmImports
805
# when the main module is loaded. If this doesn't happen then the symbols in the side module
806
# will take precedence.
807
exports = settings.WASM_EXPORTS
808
else:
809
# Ignore exported wasm globals. Those get inlined directly into the JS code.
810
exports = sorted(set(settings.WASM_EXPORTS) - set(settings.WASM_GLOBAL_EXPORTS))
811
812
extra_info = '{ "exports": [' + ','.join(f'["{asmjs_mangle(x)}", "{x}"]' for x in exports) + ']}'
813
814
txt = acorn_optimizer(js_file, ['emitDCEGraph', '--no-print'], return_output=True, extra_info=extra_info)
815
if shared.SKIP_SUBPROCS:
816
# The next steps depend on the output from this step, so we can't do them if we aren't actually running.
817
return js_file
818
graph = json.loads(txt)
819
# ensure that functions expected to be exported to the outside are roots
820
required_symbols = user_requested_exports.union(set(settings.SIDE_MODULE_IMPORTS))
821
for item in graph:
822
if 'export' in item:
823
export = asmjs_mangle(item['export'])
824
if settings.EXPORT_ALL or export in required_symbols:
825
item['root'] = True
826
827
# fix wasi imports TODO: support wasm stable with an option?
828
WASI_IMPORTS = {
829
'environ_get',
830
'environ_sizes_get',
831
'args_get',
832
'args_sizes_get',
833
'fd_write',
834
'fd_close',
835
'fd_read',
836
'fd_seek',
837
'fd_fdstat_get',
838
'fd_sync',
839
'fd_pread',
840
'fd_pwrite',
841
'proc_exit',
842
'clock_res_get',
843
'clock_time_get',
844
'path_open',
845
'random_get',
846
}
847
for item in graph:
848
if 'import' in item and item['import'][1] in WASI_IMPORTS:
849
item['import'][0] = settings.WASI_MODULE_NAME
850
851
# map import/export names to native wasm symbols.
852
import_name_map = {}
853
export_name_map = {}
854
for item in graph:
855
if 'import' in item:
856
name = item['import'][1]
857
import_name_map[item['name']] = name
858
if asmjs_mangle(name) in settings.SIDE_MODULE_IMPORTS:
859
item['root'] = True
860
elif 'export' in item:
861
export_name_map[item['name']] = item['export']
862
temp = temp_files.get('.json', prefix='emcc_dce_graph_').name
863
utils.write_file(temp, json.dumps(graph, indent=2))
864
# run wasm-metadce
865
args = ['--graph-file=' + temp]
866
if last:
867
args += get_last_binaryen_opts()
868
out = run_binaryen_command('wasm-metadce',
869
wasm_file,
870
wasm_file,
871
args,
872
debug=debug_info,
873
stdout=PIPE)
874
# find the unused things in js
875
unused_imports = []
876
unused_exports = []
877
PREFIX = 'unused: '
878
for line in out.splitlines():
879
if line.startswith(PREFIX):
880
name = line.replace(PREFIX, '').strip()
881
# With dynamic linking we never want to strip the memory or the table
882
# This can be removed once SIDE_MODULE_IMPORTS includes tables and memories.
883
if settings.MAIN_MODULE and name.split('$')[-1] in ('wasmMemory', 'wasmTable'):
884
continue
885
# we only remove imports and exports in applyDCEGraphRemovals
886
if name.startswith('emcc$import$'):
887
native_name = import_name_map[name]
888
unused_imports.append(native_name)
889
elif name.startswith('emcc$export$') and settings.DECLARE_ASM_MODULE_EXPORTS:
890
native_name = export_name_map[name]
891
if shared.is_user_export(native_name):
892
unused_exports.append(native_name)
893
if not unused_exports and not unused_imports:
894
# nothing found to be unused, so we have nothing to remove
895
return js_file
896
# remove them
897
passes = ['applyDCEGraphRemovals']
898
if settings.MINIFY_WHITESPACE:
899
passes.append('--minify-whitespace')
900
if DEBUG:
901
logger.debug("unused_imports: %s", str(unused_imports))
902
logger.debug("unused_exports: %s", str(unused_exports))
903
extra_info = {'unusedImports': unused_imports, 'unusedExports': unused_exports}
904
return acorn_optimizer(js_file, passes, extra_info=json.dumps(extra_info))
905
906
907
def asyncify_lazy_load_code(wasm_target, debug):
908
# Create the lazy-loaded wasm. Remove any active memory segments and the
909
# start function from it (as these will segments have already been applied
910
# by the initial wasm) and apply the knowledge that it will only rewind,
911
# after which optimizations can remove some code
912
args = ['--remove-memory-init', '--mod-asyncify-never-unwind']
913
if settings.OPT_LEVEL > 0:
914
args.append(opt_level_to_str(settings.OPT_LEVEL, settings.SHRINK_LEVEL))
915
run_wasm_opt(wasm_target,
916
wasm_target + '.lazy.wasm',
917
args=args,
918
debug=debug)
919
# re-optimize the original, by applying the knowledge that imports will
920
# definitely unwind, and we never rewind, after which optimizations can remove
921
# a lot of code
922
# TODO: support other asyncify stuff, imports that don't always unwind?
923
# TODO: source maps etc.
924
args = ['--mod-asyncify-always-and-only-unwind']
925
if settings.OPT_LEVEL > 0:
926
args.append(opt_level_to_str(settings.OPT_LEVEL, settings.SHRINK_LEVEL))
927
run_wasm_opt(infile=wasm_target,
928
outfile=wasm_target,
929
args=args,
930
debug=debug)
931
932
933
def minify_wasm_imports_and_exports(js_file, wasm_file, minify_exports, debug_info):
934
logger.debug('minifying wasm imports and exports')
935
# run the pass
936
args = []
937
if minify_exports:
938
# standalone wasm mode means we need to emit a wasi import module.
939
# otherwise, minify even the imported module names.
940
if settings.MINIFY_WASM_IMPORTED_MODULES:
941
args.append('--minify-imports-and-exports-and-modules')
942
else:
943
args.append('--minify-imports-and-exports')
944
else:
945
args.append('--minify-imports')
946
# this is always the last tool we run (if we run it)
947
args += get_last_binaryen_opts()
948
out = run_wasm_opt(wasm_file, wasm_file, args, debug=debug_info, stdout=PIPE)
949
950
# get the mapping
951
SEP = ' => '
952
mapping = {}
953
for line in out.split('\n'):
954
if SEP in line:
955
old, new = line.strip().split(SEP)
956
assert old not in mapping, 'imports must be unique'
957
mapping[old] = new
958
# apply them
959
passes = ['applyImportAndExportNameChanges']
960
if settings.MINIFY_WHITESPACE:
961
passes.append('--minify-whitespace')
962
extra_info = {'mapping': mapping}
963
if settings.MINIFICATION_MAP:
964
lines = [f'{new}:{old}' for old, new in mapping.items()]
965
utils.write_file(settings.MINIFICATION_MAP, '\n'.join(lines) + '\n')
966
return acorn_optimizer(js_file, passes, extra_info=json.dumps(extra_info))
967
968
969
def wasm2js(js_file, wasm_file, opt_level, use_closure_compiler, debug_info, symbols_file=None, symbols_file_js=None):
970
logger.debug('wasm2js')
971
args = ['--emscripten']
972
if opt_level > 0:
973
args += ['-O']
974
if symbols_file:
975
args += ['--symbols-file=%s' % symbols_file]
976
wasm2js_js = run_binaryen_command('wasm2js', wasm_file,
977
args=args,
978
debug=debug_info,
979
stdout=PIPE)
980
if DEBUG:
981
utils.write_file(os.path.join(get_emscripten_temp_dir(), 'wasm2js-output.js'), wasm2js_js)
982
# JS optimizations
983
if opt_level >= 2:
984
passes = []
985
if not debug_info and not settings.PTHREADS:
986
passes += ['minifyNames']
987
if symbols_file_js:
988
passes += ['symbolMap=%s' % symbols_file_js]
989
if settings.MINIFY_WHITESPACE:
990
passes += ['--minify-whitespace']
991
if passes:
992
# hackish fixups to work around wasm2js style and the js optimizer FIXME
993
wasm2js_js = f'// EMSCRIPTEN_START_ASM\n{wasm2js_js}// EMSCRIPTEN_END_ASM\n'
994
wasm2js_js = wasm2js_js.replace('\n function $', '\nfunction $')
995
wasm2js_js = wasm2js_js.replace('\n }', '\n}')
996
temp = shared.get_temp_files().get('.js').name
997
utils.write_file(temp, wasm2js_js)
998
temp = js_optimizer(temp, passes)
999
wasm2js_js = utils.read_file(temp)
1000
# Closure compiler: in mode 1, we just minify the shell. In mode 2, we
1001
# minify the wasm2js output as well, which is ok since it isn't
1002
# validating asm.js.
1003
# TODO: in the non-closure case, we could run a lightweight general-
1004
# purpose JS minifier here.
1005
if use_closure_compiler == 2:
1006
temp = shared.get_temp_files().get('.js').name
1007
with open(temp, 'a') as f:
1008
f.write(wasm2js_js)
1009
temp = closure_compiler(temp, advanced=False)
1010
wasm2js_js = utils.read_file(temp)
1011
# closure may leave a trailing `;`, which would be invalid given where we place
1012
# this code (inside parens)
1013
wasm2js_js = wasm2js_js.strip()
1014
if wasm2js_js[-1] == ';':
1015
wasm2js_js = wasm2js_js[:-1]
1016
all_js = utils.read_file(js_file)
1017
# quoted notation, something like Module['__wasm2jsInstantiate__']
1018
finds = re.findall(r'''[\w\d_$]+\[['"]__wasm2jsInstantiate__['"]\]''', all_js)
1019
if not finds:
1020
# post-closure notation, something like a.__wasm2jsInstantiate__
1021
finds = re.findall(r'''[\w\d_$]+\.__wasm2jsInstantiate__''', all_js)
1022
assert len(finds) == 1
1023
marker = finds[0]
1024
all_js = all_js.replace(marker, f'(\n{wasm2js_js}\n)')
1025
# replace the placeholder with the actual code
1026
js_file = js_file + '.wasm2js.js'
1027
utils.write_file(js_file, all_js)
1028
return js_file
1029
1030
1031
def strip(infile, outfile, debug=False, sections=None):
1032
"""Strip DWARF and/or other specified sections from a wasm file"""
1033
cmd = [LLVM_OBJCOPY, infile, outfile]
1034
if debug:
1035
cmd += ['--remove-section=.debug*']
1036
if sections:
1037
cmd += ['--remove-section=' + section for section in sections]
1038
check_call(cmd)
1039
1040
1041
# extract the DWARF info from the main file, and leave the wasm with
1042
# debug into as a file on the side
1043
def emit_debug_on_side(wasm_file, wasm_file_with_dwarf):
1044
embedded_path = settings.SEPARATE_DWARF_URL
1045
if not embedded_path:
1046
# a path was provided - make it relative to the wasm.
1047
embedded_path = os.path.relpath(wasm_file_with_dwarf,
1048
os.path.dirname(wasm_file))
1049
# normalize the path to use URL-style separators, per the spec
1050
embedded_path = utils.normalize_path(embedded_path)
1051
1052
shutil.move(wasm_file, wasm_file_with_dwarf)
1053
strip(wasm_file_with_dwarf, wasm_file, debug=True)
1054
1055
# Strip code and data from the debug file to limit its size. The other known
1056
# sections are still required to correctly interpret the DWARF info.
1057
# TODO(dschuff): Also strip the DATA section? To make this work we'd need to
1058
# either allow "invalid" data segment name entries, or maybe convert the DATA
1059
# to a DATACOUNT section.
1060
# TODO(https://github.com/emscripten-core/emscripten/issues/13084): Re-enable
1061
# this code once the debugger extension can handle wasm files with name
1062
# sections but no code sections.
1063
# strip(wasm_file_with_dwarf, wasm_file_with_dwarf, sections=['CODE'])
1064
1065
# embed a section in the main wasm to point to the file with external DWARF,
1066
# see https://yurydelendik.github.io/webassembly-dwarf/#external-DWARF
1067
section_name = b'\x13external_debug_info' # section name, including prefixed size
1068
filename_bytes = embedded_path.encode('utf-8')
1069
contents = webassembly.to_leb(len(filename_bytes)) + filename_bytes
1070
section_size = len(section_name) + len(contents)
1071
with open(wasm_file, 'ab') as f:
1072
f.write(b'\0') # user section is code 0
1073
f.write(webassembly.to_leb(section_size))
1074
f.write(section_name)
1075
f.write(contents)
1076
1077
1078
def little_endian_heap(js_file):
1079
logger.debug('enforcing little endian heap byte order')
1080
return acorn_optimizer(js_file, ['littleEndianHeap'])
1081
1082
1083
def apply_wasm_memory_growth(js_file):
1084
assert not settings.GROWABLE_ARRAYBUFFERS
1085
logger.debug('supporting wasm memory growth with pthreads')
1086
return acorn_optimizer(js_file, ['growableHeap'])
1087
1088
1089
def use_unsigned_pointers_in_js(js_file):
1090
logger.debug('using unsigned pointers in JS')
1091
return acorn_optimizer(js_file, ['unsignPointers'])
1092
1093
1094
def instrument_js_for_asan(js_file):
1095
logger.debug('instrumenting JS memory accesses for ASan')
1096
return acorn_optimizer(js_file, ['asanify'])
1097
1098
1099
def instrument_js_for_safe_heap(js_file):
1100
logger.debug('instrumenting JS memory accesses for SAFE_HEAP')
1101
return acorn_optimizer(js_file, ['safeHeap'])
1102
1103
1104
def read_name_section(wasm_file):
1105
with webassembly.Module(wasm_file) as module:
1106
for section in module.sections():
1107
if section.type == webassembly.SecType.CUSTOM:
1108
module.seek(section.offset)
1109
if module.read_string() == 'name':
1110
name_map = {}
1111
# The name section is made up sub-section.
1112
# We are looking for the function names sub-section
1113
while module.tell() < section.offset + section.size:
1114
name_type = module.read_uleb()
1115
subsection_size = module.read_uleb()
1116
subsection_end = module.tell() + subsection_size
1117
if name_type == webassembly.NameType.FUNCTION:
1118
# We found the function names sub-section
1119
num_names = module.read_uleb()
1120
for _ in range(num_names):
1121
id = module.read_uleb()
1122
name = module.read_string()
1123
name_map[id] = name
1124
return name_map
1125
module.seek(subsection_end)
1126
1127
return name_map
1128
1129
1130
@ToolchainProfiler.profile()
1131
def write_symbol_map(wasm_file, symbols_file):
1132
logger.debug('handle_final_wasm_symbols')
1133
names = read_name_section(wasm_file)
1134
assert(names)
1135
strings = [f'{id}:{name}' for id, name in names.items()]
1136
contents = '\n'.join(strings) + '\n'
1137
utils.write_file(symbols_file, contents)
1138
1139
1140
def is_ar(filename):
1141
"""Return True if a the given filename is an ar archive, False otherwise.
1142
"""
1143
try:
1144
header = open(filename, 'rb').read(8)
1145
except Exception as e:
1146
logger.debug('is_ar failed to test whether file \'%s\' is a llvm archive file! Failed on exception: %s' % (filename, e))
1147
return False
1148
1149
return header in (b'!<arch>\n', b'!<thin>\n')
1150
1151
1152
def is_wasm(filename):
1153
if not os.path.isfile(filename):
1154
return False
1155
header = open(filename, 'rb').read(webassembly.HEADER_SIZE)
1156
return header == webassembly.MAGIC + webassembly.VERSION
1157
1158
1159
def is_wasm_dylib(filename):
1160
"""Detect wasm dynamic libraries by the presence of the "dylink" custom section."""
1161
if not is_wasm(filename):
1162
return False
1163
with webassembly.Module(filename) as module:
1164
section = next(module.sections())
1165
if section.type == webassembly.SecType.CUSTOM:
1166
module.seek(section.offset)
1167
if module.read_string() in ('dylink', 'dylink.0'):
1168
return True
1169
return False
1170
1171
1172
def emit_wasm_source_map(wasm_file, map_file, final_wasm):
1173
# source file paths must be relative to the location of the map (which is
1174
# emitted alongside the wasm)
1175
base_path = os.path.dirname(os.path.abspath(final_wasm))
1176
sourcemap_cmd = [sys.executable, '-E', path_from_root('tools/wasm-sourcemap.py'),
1177
wasm_file,
1178
'--dwarfdump=' + LLVM_DWARFDUMP,
1179
'-o', map_file,
1180
'--basepath=' + base_path]
1181
1182
if settings.SOURCE_MAP_PREFIXES:
1183
sourcemap_cmd += ['--prefix', *settings.SOURCE_MAP_PREFIXES]
1184
1185
if settings.GENERATE_SOURCE_MAP == 2:
1186
sourcemap_cmd += ['--sources']
1187
1188
check_call(sourcemap_cmd)
1189
1190
1191
def get_binaryen_feature_flags():
1192
# settings.BINARYEN_FEATURES is empty unless features have been extracted by
1193
# wasm-emscripten-finalize already.
1194
if settings.BINARYEN_FEATURES:
1195
return settings.BINARYEN_FEATURES
1196
else:
1197
return ['--detect-features']
1198
1199
1200
def check_binaryen(bindir):
1201
opt = os.path.join(bindir, exe_suffix('wasm-opt'))
1202
if not os.path.exists(opt):
1203
exit_with_error('binaryen executable not found (%s). Please check your binaryen installation' % opt)
1204
try:
1205
output = run_process([opt, '--version'], stdout=PIPE).stdout
1206
except subprocess.CalledProcessError:
1207
exit_with_error('error running binaryen executable (%s). Please check your binaryen installation' % opt)
1208
if output:
1209
output = output.splitlines()[0]
1210
try:
1211
version = output.split()[2]
1212
version = int(version)
1213
except (IndexError, ValueError):
1214
exit_with_error('error parsing binaryen version (%s). Please check your binaryen installation (%s)' % (output, opt))
1215
1216
# Allow the expected version or the following one in order avoid needing to update both
1217
# emscripten and binaryen in lock step in emscripten-releases.
1218
if version not in (EXPECTED_BINARYEN_VERSION, EXPECTED_BINARYEN_VERSION + 1):
1219
diagnostics.warning('version-check', 'unexpected binaryen version: %s (expected %s)', version, EXPECTED_BINARYEN_VERSION)
1220
1221
1222
def get_binaryen_bin():
1223
global binaryen_checked
1224
rtn = os.path.join(config.BINARYEN_ROOT, 'bin')
1225
if not binaryen_checked:
1226
check_binaryen(rtn)
1227
binaryen_checked = True
1228
return rtn
1229
1230
1231
# track whether the last binaryen command kept debug info around. this is used
1232
# to see whether we need to do an extra step at the end to strip it.
1233
binaryen_kept_debug_info = False
1234
1235
1236
def run_binaryen_command(tool, infile, outfile=None, args=None, debug=False, stdout=None):
1237
cmd = [os.path.join(get_binaryen_bin(), tool)]
1238
if args:
1239
cmd += args
1240
if infile:
1241
cmd += [infile]
1242
if outfile:
1243
cmd += ['-o', outfile]
1244
if settings.ERROR_ON_WASM_CHANGES_AFTER_LINK:
1245
# emit some extra helpful text for common issues
1246
extra = ''
1247
# a plain -O0 build *almost* doesn't need post-link changes, except for
1248
# legalization. show a clear error for those (as the flags the user passed
1249
# in are not enough to see what went wrong)
1250
if settings.LEGALIZE_JS_FFI:
1251
extra += '\nnote: to disable int64 legalization (which requires changes after link) use -sWASM_BIGINT'
1252
if settings.OPT_LEVEL > 1:
1253
extra += '\nnote: -O2+ optimizations always require changes, build with -O0 or -O1 instead'
1254
exit_with_error(f'changes to the wasm are required after link, but disallowed by ERROR_ON_WASM_CHANGES_AFTER_LINK: {cmd}{extra}')
1255
if debug:
1256
cmd += ['-g'] # preserve the debug info
1257
# if the features are not already handled, handle them
1258
cmd += get_binaryen_feature_flags()
1259
# if we are emitting a source map, every time we load and save the wasm
1260
# we must tell binaryen to update it
1261
# TODO: all tools should support source maps; wasm-ctor-eval does not atm,
1262
# for example
1263
if settings.GENERATE_SOURCE_MAP and outfile and tool in ['wasm-opt', 'wasm-emscripten-finalize']:
1264
cmd += [f'--input-source-map={infile}.map']
1265
cmd += [f'--output-source-map={outfile}.map']
1266
shared.print_compiler_stage(cmd)
1267
if shared.SKIP_SUBPROCS:
1268
return ''
1269
ret = check_call(cmd, stdout=stdout).stdout
1270
if outfile:
1271
save_intermediate(outfile, '%s.wasm' % tool)
1272
global binaryen_kept_debug_info
1273
binaryen_kept_debug_info = '-g' in cmd
1274
return ret
1275
1276
1277
def run_wasm_opt(infile, outfile=None, args=[], **kwargs): # noqa
1278
return run_binaryen_command('wasm-opt', infile, outfile, args=args, **kwargs)
1279
1280
1281
intermediate_counter = 0
1282
1283
1284
def new_intermediate_filename(name):
1285
assert DEBUG
1286
global intermediate_counter
1287
basename = 'emcc-%02d-%s' % (intermediate_counter, name)
1288
intermediate_counter += 1
1289
filename = os.path.join(shared.CANONICAL_TEMP_DIR, basename)
1290
logger.debug('saving intermediate file %s' % filename)
1291
return filename
1292
1293
1294
def save_intermediate(src, name):
1295
"""Copy an existing file CANONICAL_TEMP_DIR"""
1296
if DEBUG:
1297
shutil.copyfile(src, new_intermediate_filename(name))
1298
1299
1300
def write_intermediate(content, name):
1301
"""Generate a new debug file CANONICAL_TEMP_DIR"""
1302
if DEBUG:
1303
utils.write_file(new_intermediate_filename(name), content)
1304
1305
1306
def read_and_preprocess(filename, expand_macros=False):
1307
# Create a settings file with the current settings to pass to the JS preprocessor
1308
settings_json = json.dumps(settings.external_dict(), sort_keys=True, indent=2)
1309
write_intermediate(settings_json, 'settings.json')
1310
1311
# Run the JS preprocessor
1312
dirname, filename = os.path.split(filename)
1313
if not dirname:
1314
dirname = None
1315
args = ['-', filename]
1316
if expand_macros:
1317
args += ['--expand-macros']
1318
1319
return shared.run_js_tool(path_from_root('tools/preprocessor.mjs'), args, input=settings_json, stdout=subprocess.PIPE, cwd=dirname)
1320
1321
1322
def js_legalization_pass_flags():
1323
flags = []
1324
if settings.RELOCATABLE:
1325
# When building in relocatable mode, we also want access the original
1326
# non-legalized wasm functions (since wasm modules can and do link to
1327
# the original, non-legalized, functions).
1328
flags += ['--pass-arg=legalize-js-interface-export-originals']
1329
if not settings.SIDE_MODULE:
1330
# Unless we are building a side module the helper functions should be
1331
# assumed to be defined and exports within the module, otherwise binaryen
1332
# assumes they are imports.
1333
flags += ['--pass-arg=legalize-js-interface-exported-helpers']
1334
return flags
1335
1336
1337
# Returns a list of flags to pass to emcc that make the output run properly in
1338
# the given node version.
1339
def get_emcc_node_flags(node_version):
1340
if not node_version:
1341
return []
1342
# Convert to the format we use in our settings, XXYYZZ, for example,
1343
# 10.1.7 will turn into "100107".
1344
str_node_version = "%02d%02d%02d" % node_version
1345
return [f'-sMIN_NODE_VERSION={str_node_version}']
1346
1347