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