Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/tools/wasm-sourcemap.py
6179 views
1
#!/usr/bin/env python3
2
# Copyright 2018 The Emscripten Authors. All rights reserved.
3
# Emscripten is available under two separate licenses, the MIT license and the
4
# University of Illinois/NCSA Open Source License. Both these licenses can be
5
# found in the LICENSE file.
6
7
"""Utility tools that extracts DWARF information encoded in a wasm output
8
produced by the LLVM tools, and encodes it as a wasm source map. Additionally,
9
it can collect original sources, change files prefixes, and strip debug
10
sections from a wasm file.
11
"""
12
13
import argparse
14
import json
15
import logging
16
import os
17
import re
18
import sys
19
from math import floor, log
20
21
__scriptdir__ = os.path.dirname(os.path.abspath(__file__))
22
__rootdir__ = os.path.dirname(__scriptdir__)
23
sys.path.insert(0, __rootdir__)
24
25
from tools import shared, utils
26
from tools.system_libs import DETERMINISTIC_PREFIX
27
28
LLVM_CXXFILT = shared.llvm_tool_path('llvm-cxxfilt')
29
30
EMSCRIPTEN_PREFIX = utils.normalize_path(utils.path_from_root())
31
32
logger = logging.getLogger('wasm-sourcemap')
33
34
# FIXME: Generate Scopes info
35
generate_scopes = False
36
37
38
def parse_args(args):
39
parser = argparse.ArgumentParser(prog='wasm-sourcemap.py', description=__doc__)
40
parser.add_argument('wasm', help='wasm file')
41
parser.add_argument('-o', '--output', help='output source map')
42
parser.add_argument('-p', '--prefix', nargs='*', help='replace source debug filename prefix for source map', default=[])
43
parser.add_argument('-s', '--sources', action='store_true', help='read and embed source files from file system into source map')
44
parser.add_argument('-l', '--load-prefix', nargs='*', help='replace source debug filename prefix for reading sources from file system (see also --sources)', default=[])
45
parser.add_argument('-w', nargs='?', help='set output wasm file')
46
parser.add_argument('-x', '--strip', action='store_true', help='removes debug and linking sections')
47
parser.add_argument('-u', '--source-map-url', nargs='?', help='specifies sourceMappingURL section content')
48
parser.add_argument('--dwarfdump', help="path to llvm-dwarfdump executable")
49
parser.add_argument('--dwarfdump-output', nargs='?', help=argparse.SUPPRESS)
50
parser.add_argument('--basepath', help='base path for source files, which will be relative to this')
51
return parser.parse_args(args)
52
53
54
class Prefixes:
55
def __init__(self, args, base_path=None, preserve_deterministic_prefix=True):
56
prefixes = []
57
for p in args:
58
if '=' in p:
59
prefix, replacement = p.split('=')
60
prefixes.append({'prefix': utils.normalize_path(prefix), 'replacement': replacement})
61
else:
62
prefixes.append({'prefix': utils.normalize_path(p), 'replacement': ''})
63
self.base_path = utils.normalize_path(base_path) if base_path is not None else None
64
self.preserve_deterministic_prefix = preserve_deterministic_prefix
65
self.prefixes = prefixes
66
self.cache = {}
67
68
def resolve(self, name):
69
if name in self.cache:
70
return self.cache[name]
71
72
source = name
73
if not self.preserve_deterministic_prefix and name.startswith(DETERMINISTIC_PREFIX):
74
source = EMSCRIPTEN_PREFIX + name.removeprefix(DETERMINISTIC_PREFIX)
75
76
provided = False
77
for p in self.prefixes:
78
if source.startswith(p['prefix']):
79
source = p['replacement'] + source.removeprefix(p['prefix'])
80
provided = True
81
break
82
83
# If prefixes were provided, we use that; otherwise if base_path is set, we
84
# emit a relative path. For files with deterministic prefix, we never use
85
# a relative path, precisely to preserve determinism, and because it would
86
# still point to the wrong location, so we leave the filepath untouched to
87
# let users map it to the proper location using prefix options.
88
if not (source.startswith(DETERMINISTIC_PREFIX) or provided or self.base_path is None):
89
try:
90
source = os.path.relpath(source, self.base_path)
91
except ValueError:
92
source = os.path.abspath(source)
93
source = utils.normalize_path(source)
94
95
self.cache[name] = source
96
return source
97
98
99
# SourceMapPrefixes contains resolver for file names that are:
100
# - "sources" is for names that output to source maps JSON
101
# - "load" is for paths that used to load source text
102
class SourceMapPrefixes:
103
def __init__(self, sources, load, base_path):
104
self.sources = Prefixes(sources, base_path=base_path)
105
self.load = Prefixes(load, preserve_deterministic_prefix=False)
106
107
108
def encode_vlq(n):
109
VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
110
x = (n << 1) if n >= 0 else ((-n << 1) + 1)
111
result = ""
112
while x > 31:
113
result = result + VLQ_CHARS[32 + (x & 31)]
114
x = x >> 5
115
return result + VLQ_CHARS[x]
116
117
118
def read_var_uint(wasm, pos):
119
n = 0
120
shift = 0
121
b = ord(wasm[pos:pos + 1])
122
pos = pos + 1
123
while b >= 128:
124
n = n | ((b - 128) << shift)
125
b = ord(wasm[pos:pos + 1])
126
pos = pos + 1
127
shift += 7
128
return n + (b << shift), pos
129
130
131
def strip_debug_sections(wasm):
132
logger.debug('Strip debug sections')
133
pos = 8
134
stripped = wasm[:pos]
135
136
while pos < len(wasm):
137
section_start = pos
138
section_id, pos_ = read_var_uint(wasm, pos)
139
section_size, section_body = read_var_uint(wasm, pos_)
140
pos = section_body + section_size
141
if section_id == 0:
142
name_len, name_pos = read_var_uint(wasm, section_body)
143
name_end = name_pos + name_len
144
name = str(wasm[name_pos:name_end])
145
if name in {'linking', 'sourceMappingURL'} or name.startswith(('reloc..debug_', '.debug_')):
146
continue # skip debug related sections
147
stripped = stripped + wasm[section_start:pos]
148
149
return stripped
150
151
152
def encode_uint_var(n):
153
result = bytearray()
154
while n > 127:
155
result.append(128 | (n & 127))
156
n = n >> 7
157
result.append(n)
158
return bytes(result)
159
160
161
def append_source_mapping(wasm, url):
162
logger.debug('Append sourceMappingURL section')
163
section_name = "sourceMappingURL"
164
section_content = encode_uint_var(len(section_name)) + section_name.encode() + encode_uint_var(len(url)) + url.encode()
165
return wasm + encode_uint_var(0) + encode_uint_var(len(section_content)) + section_content
166
167
168
def get_code_section_offset(wasm):
169
logger.debug('Read sections index')
170
pos = 8
171
172
while pos < len(wasm):
173
section_id, pos_ = read_var_uint(wasm, pos)
174
section_size, pos = read_var_uint(wasm, pos_)
175
if section_id == 10:
176
return pos
177
pos = pos + section_size
178
179
180
def remove_dead_entries(entries):
181
# Remove entries for dead functions. It is a heuristics to ignore data if the
182
# function starting address near to 0 (is equal to its size field length).
183
block_start = 0
184
cur_entry = 0
185
while cur_entry < len(entries):
186
if not entries[cur_entry]['eos']:
187
cur_entry += 1
188
continue
189
fn_start = entries[block_start]['address']
190
# Calculate the LEB encoded function size (including size field)
191
fn_size_length = floor(log(entries[cur_entry]['address'] - fn_start + 1, 128)) + 1
192
min_live_offset = 1 + fn_size_length # 1 byte is for code section entries
193
if fn_start < min_live_offset:
194
# Remove dead code debug info block.
195
del entries[block_start:cur_entry + 1]
196
cur_entry = block_start
197
continue
198
cur_entry += 1
199
block_start = cur_entry
200
201
202
# Given a string that has non-ASCII UTF-8 bytes 128-255 stored as octal sequences (\200 - \377), decode
203
# the sequences back to UTF-8. E.g. "C:\\\303\244 \303\266\\emsdk\\emscripten\\main" -> "C:\\ä ö\\emsdk\\emscripten\\main"
204
def decode_octal_encoded_utf8(str):
205
out = bytearray(len(str))
206
i = 0
207
o = 0
208
final_length = len(str)
209
in_escape = False
210
while i < len(str):
211
if not in_escape and str[i] == '\\' and (str[i + 1] == '2' or str[i + 1] == '3'):
212
out[o] = int(str[i + 1:i + 4], 8)
213
i += 4
214
final_length -= 3
215
in_escape = False
216
else:
217
out[o] = ord(str[i])
218
in_escape = False if in_escape else (str[i] == '\\')
219
i += 1
220
o += 1
221
return out[:final_length].decode('utf-8')
222
223
224
def extract_comp_dir_map(text):
225
compile_unit_pattern = re.compile(r"0x[0-9a-f]*: DW_TAG_compile_unit")
226
stmt_list_pattern = re.compile(r"DW_AT_stmt_list\s+\((0x[0-9a-f]*)\)")
227
comp_dir_pattern = re.compile(r"DW_AT_comp_dir\s+\(\"([^\"]+)\"\)")
228
229
map_stmt_list_to_comp_dir = {}
230
iterator = compile_unit_pattern.finditer(text)
231
current_match = next(iterator, None)
232
233
while current_match:
234
next_match = next(iterator, None)
235
start = current_match.end()
236
end = next_match.start() if next_match else len(text)
237
238
stmt_list_match = stmt_list_pattern.search(text, start, end)
239
if stmt_list_match is not None:
240
stmt_list = stmt_list_match.group(1)
241
comp_dir_match = comp_dir_pattern.search(text, start, end)
242
comp_dir = decode_octal_encoded_utf8(comp_dir_match.group(1)) if comp_dir_match is not None else ''
243
map_stmt_list_to_comp_dir[stmt_list] = comp_dir
244
245
current_match = next_match
246
247
return map_stmt_list_to_comp_dir
248
249
250
def demangle_names(names):
251
# Only demangle names that look mangled
252
mangled_names = sorted({n for n in names if n.startswith('_Z')})
253
if not mangled_names:
254
return {}
255
if not os.path.exists(LLVM_CXXFILT):
256
logger.warning('llvm-cxxfilt does not exist')
257
return {}
258
259
# Gather all mangled names and call llvm-cxxfilt only once for all of them
260
input_str = '\n'.join(mangled_names)
261
proc = shared.check_call([LLVM_CXXFILT], input=input_str, stdout=shared.PIPE, stderr=shared.PIPE, text=True)
262
if proc.returncode != 0:
263
logger.warning('llvm-cxxfilt failed: %s' % proc.stderr)
264
return {}
265
266
demangled_list = proc.stdout.splitlines()
267
if len(demangled_list) != len(mangled_names):
268
logger.warning('llvm-cxxfilt output length mismatch')
269
return {}
270
271
return dict(zip(mangled_names, demangled_list, strict=True))
272
273
274
class FuncRange:
275
def __init__(self, name, low_pc, high_pc):
276
self.name = name
277
self.low_pc = low_pc
278
self.high_pc = high_pc
279
280
281
# This function parses DW_TAG_subprogram entries and gets low_pc and high_pc for
282
# each function in a list of FuncRanges. The result list will be sorted in the
283
# increasing order of low_pcs.
284
def extract_func_ranges(text):
285
# This function handles four cases:
286
# 1. DW_TAG_subprogram with DW_AT_name, DW_AT_low_pc, and DW_AT_high_pc.
287
# 0x000000ba: DW_TAG_subprogram
288
# DW_AT_low_pc (0x0000005f)
289
# DW_AT_high_pc (0x00000071)
290
# DW_AT_name ("foo")
291
# ...
292
#
293
# 2. DW_TAG_subprogram with DW_AT_linkage_name, DW_AT_low_pc, and
294
# DW_AT_high_pc. Applies to mangled C++ functions.
295
# (We parse DW_AT_linkage_name instead of DW_AT_name here.)
296
# 0x000000ba: DW_TAG_subprogram
297
# DW_AT_low_pc (0x0000005f)
298
# DW_AT_high_pc (0x00000071)
299
# DW_AT_linkage_name ("_ZN7MyClass3fooEv")
300
# DW_AT_name ("foo")
301
# ...
302
#
303
# 3. DW_TAG_subprogram with DW_AT_specification, DW_AT_low_pc, and
304
# DW_AT_high_pc. C++ function info can be split into two DIEs (one with
305
# DW_AT_linkage_name and DW_AT_declaration (true) and the other with
306
# DW_AT_specification). In this case we parse DW_AT_specification for the
307
# function name.
308
# 0x0000006d: DW_TAG_subprogram
309
# DW_AT_linkage_name ("_ZN7MyClass3fooEv")
310
# DW_AT_name ("foo")
311
# DW_AT_declaration (true)
312
# ...
313
# 0x00000097: DW_TAG_subprogram
314
# DW_AT_low_pc (0x00000007)
315
# DW_AT_high_pc (0x0000004c)
316
# DW_AT_specification (0x0000006d "_ZN7MyClass3fooEv")
317
# ...
318
#
319
# 4. DW_TAG_inlined_subroutine with DW_AT_abstract_origin, DW_AT_low_pc, and
320
# DW_AT_high_pc. This represents an inlined function. We parse
321
# DW_AT_abstract_origin for the original function name.
322
# 0x0000011a: DW_TAG_inlined_subroutine
323
# DW_AT_abstract_origin (0x000000da "_ZN7MyClass3barEv")
324
# DW_AT_low_pc (0x00000078)
325
# DW_AT_high_pc (0x00000083)
326
# ...
327
328
# Pattern to find the start of the NEXT DWARF tag (boundary marker)
329
next_tag_pattern = re.compile(r'\n0x[0-9a-f]+:')
330
# Pattern to find DWARF tags for functions (Subprogram or Inlined) directly
331
func_pattern = re.compile(r'DW_TAG_(?:subprogram|inlined_subroutine)')
332
333
low_pc_pattern = re.compile(r'DW_AT_low_pc\s+\(0x([0-9a-f]+)\)')
334
high_pc_pattern = re.compile(r'DW_AT_high_pc\s+\(0x([0-9a-f]+)\)')
335
abstract_origin_pattern = re.compile(r'DW_AT_abstract_origin\s+\(0x[0-9a-f]+\s+"([^"]+)"\)')
336
linkage_name_pattern = re.compile(r'DW_AT_linkage_name\s+\("([^"]+)"\)')
337
name_pattern = re.compile(r'DW_AT_name\s+\("([^"]+)"\)')
338
specification_pattern = re.compile(r'DW_AT_specification\s+\(0x[0-9a-f]+\s+"([^"]+)"\)')
339
340
def get_name_from_tag(start, end):
341
m = linkage_name_pattern.search(text, start, end)
342
if m:
343
return m.group(1)
344
m = name_pattern.search(text, start, end)
345
if m:
346
return m.group(1)
347
# If name is missing, check for DW_AT_specification annotation
348
m = specification_pattern.search(text, start, end)
349
if m:
350
return m.group(1)
351
return None
352
353
func_ranges = []
354
for match in func_pattern.finditer(text):
355
# Search from the end of the tag name (e.g. after "DW_TAG_subprogram").
356
# Attributes are expected to follow.
357
search_start = match.end()
358
359
# Search until the beginning of the next tag
360
m_next = next_tag_pattern.search(text, search_start)
361
search_end = m_next.start() if m_next else len(text)
362
363
name = None
364
low_pc = None
365
high_pc = None
366
m = low_pc_pattern.search(text, search_start, search_end)
367
if m:
368
low_pc = int(m.group(1), 16)
369
m = high_pc_pattern.search(text, search_start, search_end)
370
if m:
371
high_pc = int(m.group(1), 16)
372
373
if 'DW_TAG_subprogram' in match.group(0):
374
name = get_name_from_tag(search_start, search_end)
375
else: # is_inlined
376
m = abstract_origin_pattern.search(text, search_start, search_end)
377
if m:
378
name = m.group(1)
379
380
if name and low_pc is not None and high_pc is not None:
381
func_ranges.append(FuncRange(name, low_pc, high_pc))
382
383
# Demangle names
384
all_names = [item.name for item in func_ranges]
385
demangled_map = demangle_names(all_names)
386
for func_range in func_ranges:
387
if func_range.name in demangled_map:
388
func_range.name = demangled_map[func_range.name]
389
390
# To correctly identify the innermost function for a given address,
391
# func_ranges is sorted primarily by low_pc in ascending order and secondarily
392
# by high_pc in descending order. This ensures that for overlapping ranges,
393
# the more specific (inner) range appears later in the list.
394
func_ranges.sort(key=lambda item: (item.low_pc, -item.high_pc))
395
return func_ranges
396
397
398
def read_dwarf_info(wasm, options):
399
if options.dwarfdump_output:
400
output = utils.read_file(options.dwarfdump_output)
401
elif options.dwarfdump:
402
logger.debug('Reading DWARF information from %s' % wasm)
403
if not os.path.exists(options.dwarfdump):
404
utils.exit_with_error('llvm-dwarfdump not found: ' + options.dwarfdump)
405
dwarfdump_cmd = [options.dwarfdump, '-debug-info', '-debug-line', wasm]
406
if generate_scopes:
407
# We need only three tags in the debug info: DW_TAG_compile_unit for
408
# source location, and DW_TAG_subprogram and DW_TAG_inlined_subroutine
409
# for the function ranges.
410
dwarfdump_cmd += ['-t', 'DW_TAG_compile_unit', '-t', 'DW_TAG_subprogram',
411
'-t', 'DW_TAG_inlined_subroutine']
412
else:
413
# We only need the top-level DW_TAG_compile_unit tags when not generating
414
# the names field
415
dwarfdump_cmd += ['--recurse-depth=0']
416
proc = shared.check_call(dwarfdump_cmd, stdout=shared.PIPE)
417
output = proc.stdout
418
else:
419
utils.exit_with_error('Please specify either --dwarfdump or --dwarfdump-output')
420
421
debug_line_pattern = re.compile(r"debug_line\[(0x[0-9a-f]*)\]")
422
include_dir_pattern = re.compile(r"include_directories\[\s*(\d+)\] = \"([^\"]*)")
423
file_pattern = re.compile(r"file_names\[\s*(\d+)\]:\s+name: \"([^\"]*)\"\s+dir_index: (\d+)")
424
line_pattern = re.compile(r"\n0x([0-9a-f]+)\s+(\d+)\s+(\d+)\s+(\d+)(.*?end_sequence)?")
425
426
entries = []
427
iterator = debug_line_pattern.finditer(output)
428
current_match = None
429
try:
430
current_match = next(iterator)
431
debug_info_end = current_match.start() # end of .debug_info contents
432
except StopIteration:
433
debug_info_end = len(output)
434
435
debug_info = output[:debug_info_end] # .debug_info contents
436
map_stmt_list_to_comp_dir = extract_comp_dir_map(debug_info)
437
438
while current_match:
439
next_match = next(iterator, None)
440
441
stmt_list = current_match.group(1)
442
start = current_match.end()
443
end = next_match.start() if next_match else len(output)
444
445
comp_dir = map_stmt_list_to_comp_dir.get(stmt_list, '')
446
447
# include_directories[ 1] = "/Users/yury/Work/junk/sqlite-playground/src"
448
# file_names[ 1]:
449
# name: "playground.c"
450
# dir_index: 1
451
# mod_time: 0x00000000
452
# length: 0x00000000
453
#
454
# Address Line Column File ISA Discriminator Flags
455
# ------------------ ------ ------ ------ --- ------------- -------------
456
# 0x0000000000000006 22 0 1 0 0 is_stmt
457
# 0x0000000000000007 23 10 1 0 0 is_stmt prologue_end
458
# 0x000000000000000f 23 3 1 0 0
459
# 0x0000000000000010 23 3 1 0 0 end_sequence
460
# 0x0000000000000011 28 0 1 0 0 is_stmt
461
462
include_directories = {'0': comp_dir}
463
for dir in include_dir_pattern.finditer(output, start, end):
464
include_directories[dir.group(1)] = os.path.join(comp_dir, decode_octal_encoded_utf8(dir.group(2)))
465
466
files = {}
467
for file in file_pattern.finditer(output, start, end):
468
dir = include_directories[file.group(3)]
469
file_path = os.path.join(dir, decode_octal_encoded_utf8(file.group(2)))
470
files[file.group(1)] = file_path
471
472
for line in line_pattern.finditer(output, start, end):
473
entry = {'address': int(line.group(1), 16), 'line': int(line.group(2)), 'column': int(line.group(3)), 'file': files[line.group(4)], 'eos': line.group(5) is not None}
474
if not entry['eos']:
475
entries.append(entry)
476
else:
477
# move end of function to the last END operator
478
entry['address'] -= 1
479
if entries[-1]['address'] == entry['address']:
480
# last entry has the same address, reusing
481
entries[-1]['eos'] = True
482
else:
483
entries.append(entry)
484
485
current_match = next_match
486
487
remove_dead_entries(entries)
488
489
# return entries sorted by the address field
490
entries = sorted(entries, key=lambda entry: entry['address'])
491
492
if generate_scopes:
493
func_ranges = extract_func_ranges(debug_info)
494
else:
495
func_ranges = []
496
return entries, func_ranges
497
498
499
def build_sourcemap(entries, func_ranges, code_section_offset, options):
500
base_path = options.basepath
501
collect_sources = options.sources
502
prefixes = SourceMapPrefixes(options.prefix, options.load_prefix, base_path)
503
504
# Add code section offset to the low/high pc in the function PC ranges
505
for func_range in func_ranges:
506
func_range.low_pc += code_section_offset
507
func_range.high_pc += code_section_offset
508
509
sources = []
510
sources_content = []
511
# There can be duplicate names in case an original source function has
512
# multiple disjoint PC ranges or is inlined to multiple callsites. Make the
513
# 'names' list a unique list of names, and map the function ranges to the
514
# indices in that list.
515
names = sorted({item.name for item in func_ranges})
516
name_to_id = {name: i for i, name in enumerate(names)}
517
mappings = []
518
sources_map = {}
519
last_address = 0
520
last_source_id = 0
521
last_line = 1
522
last_column = 1
523
last_func_id = 0
524
525
active_funcs = []
526
next_func_range_id = 0
527
528
# Get the function name ID that the given address falls into
529
def get_function_id(address):
530
nonlocal active_funcs
531
nonlocal next_func_range_id
532
533
# Maintain a list of "active functions" whose ranges currently cover the
534
# address. As the address advances, it adds new functions that start and
535
# removes functions that end. The last function remaining in the active list
536
# at any point is the innermost function.
537
while next_func_range_id < len(func_ranges) and func_ranges[next_func_range_id].low_pc <= address:
538
# active_funcs contains (high_pc, id) pair
539
active_funcs.append((func_ranges[next_func_range_id].high_pc, next_func_range_id))
540
next_func_range_id += 1
541
active_funcs = [f for f in active_funcs if f[0] > address]
542
543
if active_funcs:
544
func_range_id = active_funcs[-1][1]
545
name = func_ranges[func_range_id].name
546
return name_to_id[name]
547
return None
548
549
for entry in entries:
550
line = entry['line']
551
column = entry['column']
552
# ignore entries with line 0
553
if line == 0:
554
continue
555
# start at least at column 1
556
if column == 0:
557
column = 1
558
559
address = entry['address'] + code_section_offset
560
file_name = utils.normalize_path(entry['file'])
561
source_name = prefixes.sources.resolve(file_name)
562
563
if source_name not in sources_map:
564
source_id = len(sources)
565
sources_map[source_name] = source_id
566
sources.append(source_name)
567
if collect_sources:
568
load_name = prefixes.load.resolve(file_name)
569
try:
570
with open(load_name) as infile:
571
source_content = infile.read()
572
sources_content.append(source_content)
573
except OSError:
574
print('Failed to read source: %s' % load_name)
575
sources_content.append(None)
576
else:
577
source_id = sources_map[source_name]
578
func_id = get_function_id(address)
579
580
address_delta = address - last_address
581
source_id_delta = source_id - last_source_id
582
line_delta = line - last_line
583
column_delta = column - last_column
584
last_address = address
585
last_source_id = source_id
586
last_line = line
587
last_column = column
588
mapping = encode_vlq(address_delta) + encode_vlq(source_id_delta) + encode_vlq(line_delta) + encode_vlq(column_delta)
589
if func_id is not None:
590
func_id_delta = func_id - last_func_id
591
last_func_id = func_id
592
mapping += encode_vlq(func_id_delta)
593
mappings.append(mapping)
594
595
return {'version': 3,
596
'sources': sources,
597
'sourcesContent': sources_content,
598
'names': names,
599
'mappings': ','.join(mappings)}
600
601
602
def main(args):
603
options = parse_args(args)
604
605
wasm_input = options.wasm
606
with open(wasm_input, 'rb') as infile:
607
wasm = infile.read()
608
609
entries, func_ranges = read_dwarf_info(wasm_input, options)
610
611
code_section_offset = get_code_section_offset(wasm)
612
613
logger.debug('Saving to %s' % options.output)
614
map = build_sourcemap(entries, func_ranges, code_section_offset, options)
615
with open(options.output, 'w', encoding='utf-8') as outfile:
616
json.dump(map, outfile, separators=(',', ':'), ensure_ascii=False)
617
618
if options.strip:
619
wasm = strip_debug_sections(wasm)
620
621
if options.source_map_url:
622
wasm = append_source_mapping(wasm, options.source_map_url)
623
624
if options.w:
625
logger.debug('Saving wasm to %s' % options.w)
626
with open(options.w, 'wb') as outfile:
627
outfile.write(wasm)
628
629
logger.debug('Done')
630
return 0
631
632
633
if __name__ == '__main__':
634
logging.basicConfig(level=logging.DEBUG if os.environ.get('EMCC_DEBUG') else logging.INFO)
635
sys.exit(main(sys.argv[1:]))
636
637