Skip to content

Commit d94e591

Browse files
yurydelendiksbc100
authored andcommitted
Add wasm-sourcemap.py tool (emscripten-core#6648)
1 parent c260e80 commit d94e591

5 files changed

Lines changed: 293 additions & 0 deletions

File tree

620 Bytes
Binary file not shown.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
foo.wasm: file format WASM
2+
3+
.debug_line contents:
4+
debug_line[0x00000000]
5+
Line table prologue:
6+
total_length: 0x00000038
7+
version: 4
8+
prologue_length: 0x00000021
9+
min_inst_length: 1
10+
max_ops_per_inst: 1
11+
default_is_stmt: 1
12+
line_base: -5
13+
line_range: 14
14+
opcode_base: 13
15+
standard_opcode_lengths[DW_LNS_copy] = 0
16+
standard_opcode_lengths[DW_LNS_advance_pc] = 1
17+
standard_opcode_lengths[DW_LNS_advance_line] = 1
18+
standard_opcode_lengths[DW_LNS_set_file] = 1
19+
standard_opcode_lengths[DW_LNS_set_column] = 1
20+
standard_opcode_lengths[DW_LNS_negate_stmt] = 0
21+
standard_opcode_lengths[DW_LNS_set_basic_block] = 0
22+
standard_opcode_lengths[DW_LNS_const_add_pc] = 0
23+
standard_opcode_lengths[DW_LNS_fixed_advance_pc] = 1
24+
standard_opcode_lengths[DW_LNS_set_prologue_end] = 0
25+
standard_opcode_lengths[DW_LNS_set_epilogue_begin] = 0
26+
standard_opcode_lengths[DW_LNS_set_isa] = 1
27+
file_names[ 1]:
28+
name: "no_main.c"
29+
dir_index: 0
30+
mod_time: 0x00000000
31+
length: 0x00000000
32+
33+
Address Line Column File ISA Discriminator Flags
34+
------------------ ------ ------ ------ --- ------------- -------------
35+
0x0000000000000005 4 0 1 0 0 is_stmt
36+
0x000000000000000c 5 3 1 0 0 is_stmt prologue_end
37+
0x0000000000000010 5 3 1 0 0 is_stmt end_sequence
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//clang --target=wasm32-unknown-unknown-wasm \
2+
// -nostartfiles -nostdlib -Wl,--no-entry,--export=foo -g -o foo.wasm no_main.c
3+
int foo()
4+
{
5+
return 42;
6+
}

tests/test_other.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8389,3 +8389,19 @@ def test_js_optimizer_parse_error(self):
83898389
^
83908390
''', output.stderr)
83918391

8392+
def test_wasm_sourcemap(self):
8393+
# The no_main.c will be read (from relative location) due to speficied "-s"
8394+
shutil.copyfile(path_from_root('tests', 'other', 'wasm_sourcemap', 'no_main.c'), 'no_main.c')
8395+
wasm_map_cmd = [PYTHON, path_from_root('tools', 'wasm-sourcemap.py'),
8396+
'--sources', '--prefix', '=wasm-src:///',
8397+
'--llvm-dwarfdump', path_from_root('tests', 'other', 'wasm_sourcemap', 'foo.wasm.dump'),
8398+
'-o', 'a.out.wasm.map',
8399+
path_from_root('tests', 'other', 'wasm_sourcemap', 'foo.wasm')]
8400+
subprocess.check_call(wasm_map_cmd)
8401+
output = open('a.out.wasm.map').read()
8402+
# has "sources" entry with file (includes also `--prefix =wasm-src:///` replacement)
8403+
self.assertContained('wasm-src:///no_main.c', output)
8404+
# has "sourcesContent" entry with source code (included with `-s` option)
8405+
self.assertContained('int foo()', output)
8406+
# has some entries
8407+
self.assertIsNotNone(re.search(r'"mappings": "[A-Za-z0-9+/]', output))

tools/wasm-sourcemap.py

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
#!/usr/bin/env python
2+
3+
# Utility tools that extracts DWARF information encoded in a wasm output
4+
# produced by the LLVM tools, and encodes it as a wasm source map. Additionally,
5+
# it can collect original sources, change files prefixes, and strip debug
6+
# sections from a wasm file.
7+
8+
from subprocess import Popen, PIPE
9+
import re
10+
import json
11+
import argparse
12+
import os, sys
13+
14+
sys.path.insert(1, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15+
16+
from tools.shared import LLVM_ROOT
17+
18+
def parse_args():
19+
parser = argparse.ArgumentParser(prog='wasm-sourcemap.py')
20+
parser.add_argument('wasm', help='wasm file')
21+
parser.add_argument('-o', '--output', help='output source map')
22+
parser.add_argument('-p', '--prefix', nargs='*', help='replace source filename prefix', default=[])
23+
parser.add_argument('-s', '--sources', action='store_true', help='read and embed source files')
24+
parser.add_argument('-w', nargs='?', help='set output wasm file')
25+
parser.add_argument('-x', '--strip', action='store_true', help='removes debug and linking sections')
26+
parser.add_argument('-u', '--source-map-url', nargs='?', help='specifies sourceMappingURL section contest')
27+
parser.add_argument('--llvm-dwarfdump', nargs='?', help=argparse.SUPPRESS)
28+
return parser.parse_args()
29+
30+
VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
31+
def encode_vlq(n):
32+
x = (n << 1) if n >= 0 else ((-n << 1) + 1)
33+
result = ""
34+
while x > 31:
35+
result = result + VLQ_CHARS[32 + (x & 31)]
36+
x = x >> 5
37+
return result + VLQ_CHARS[x]
38+
39+
def read_var_uint(wasm, pos):
40+
n = 0
41+
shift = 0
42+
b = ord(wasm[pos:pos + 1])
43+
pos = pos + 1
44+
while b >= 128:
45+
n = n | ((b - 128) << shift)
46+
b = ord(wasm[pos:pos + 1])
47+
pos = pos + 1
48+
shift += 7
49+
return n + (b << shift), pos
50+
51+
def strip_debug_sections(wasm):
52+
print('Strip debug sections')
53+
pos = 8
54+
stripped = wasm[:pos]
55+
56+
while pos < len(wasm):
57+
section_start = pos
58+
section_id, pos_ = read_var_uint(wasm, pos)
59+
section_size, section_body = read_var_uint(wasm, pos_)
60+
pos = section_body + section_size
61+
if section_id == 0:
62+
name_len, name_pos = read_var_uint(wasm, section_body)
63+
name_end = name_pos + name_len
64+
name = wasm[name_pos:name_end]
65+
if name == "linking" or name == "sourceMappingURL" or name.startswith("reloc..debug_") or name.startswith(".debug_"):
66+
continue # skip debug related sections
67+
stripped = stripped + wasm[section_start:pos]
68+
69+
return stripped
70+
71+
def encode_uint_var(n):
72+
result = bytearray()
73+
while n > 127:
74+
result.append(128 | (n & 127))
75+
n = n >> 7
76+
result.append(n)
77+
return bytes(result)
78+
79+
def append_source_mapping(wasm, url):
80+
print('Append sourceMappingURL section')
81+
section_name = "sourceMappingURL"
82+
section_content = encode_uint_var(len(section_name)) + section_name + encode_uint_var(len(url)) + url
83+
return wasm + encode_uint_var(0) + encode_uint_var(len(section_content)) + section_content
84+
85+
def get_code_section_offset(wasm):
86+
print('Read sections index')
87+
pos = 8
88+
89+
while pos < len(wasm):
90+
section_id, pos_ = read_var_uint(wasm, pos)
91+
section_size, pos = read_var_uint(wasm, pos_)
92+
if section_id == 10:
93+
return pos
94+
pos = pos + section_size
95+
96+
def read_dwarf_entries(wasm, dwarfdump):
97+
if dwarfdump:
98+
output = open(dwarfdump, 'r').read()
99+
else:
100+
print('Reading DWARF information from %s' % wasm)
101+
llvm_dwarfdump = os.path.join(LLVM_ROOT, 'llvm-dwarfdump')
102+
process = Popen([llvm_dwarfdump, "-debug-line", wasm], stdout=PIPE)
103+
(output, err) = process.communicate()
104+
exit_code = process.wait()
105+
if exit_code != 0:
106+
print('Error during llvm-dwarfdump execution (%s)' % exit_code)
107+
exit(1)
108+
109+
entries = []
110+
debug_line_chunks = re.split(r"(debug_line\[0x[0-9a-f]*\])", output)
111+
for i in range(1,len(debug_line_chunks),2):
112+
line_chunk = debug_line_chunks[i + 1]
113+
114+
# include_directories[ 1] = "/Users/yury/Work/junk/sqlite-playground/src"
115+
# file_names[ 1]:
116+
# name: "playground.c"
117+
# dir_index: 1
118+
# mod_time: 0x00000000
119+
# length: 0x00000000
120+
#
121+
# Address Line Column File ISA Discriminator Flags
122+
# ------------------ ------ ------ ------ --- ------------- -------------
123+
# 0x0000000000000006 22 0 1 0 0 is_stmt
124+
# 0x0000000000000007 23 10 1 0 0 is_stmt prologue_end
125+
# 0x000000000000000f 23 3 1 0 0
126+
# 0x0000000000000010 23 3 1 0 0 end_sequence
127+
# 0x0000000000000011 28 0 1 0 0 is_stmt
128+
129+
include_directories = {'0': ""}
130+
for dir in re.finditer(r"include_directories\[\s*(\d+)\] = \"([^\"]*)", line_chunk):
131+
include_directories[dir.group(1)] = dir.group(2)
132+
133+
files = {}
134+
for file in re.finditer(r"file_names\[\s*(\d+)\]:\s+name: \"([^\"]*)\"\s+dir_index: (\d+)", line_chunk):
135+
dir = include_directories[file.group(3)]
136+
file_path = (dir + '/' if dir != '' else '') + file.group(2)
137+
files[file.group(1)] = file_path
138+
139+
140+
for line in re.finditer(r"\n0x([0-9a-f]+)\s+(\d+)\s+(\d+)\s+(\d+)", line_chunk):
141+
entry = {'address': int(line.group(1), 16), 'line': int(line.group(2)), 'column': int(line.group(3)), 'file': files[line.group(4)]}
142+
entries.append(entry)
143+
return entries
144+
145+
def build_sourcemap(entries, code_section_offset, prefixes, collect_sources):
146+
sources = []
147+
sources_content = [] if collect_sources else None
148+
mappings = []
149+
150+
sources_map = {}
151+
last_address = 0
152+
last_source_id = 0
153+
last_line = 1
154+
last_column = 1
155+
for entry in entries:
156+
line = entry['line']
157+
column = entry['column']
158+
if line == 0 or column == 0:
159+
continue
160+
address = entry['address'] + code_section_offset
161+
file_name = entry['file']
162+
if file_name not in sources_map:
163+
source_id = len(sources)
164+
sources_map[file_name] = source_id
165+
source_name = file_name
166+
for p in prefixes:
167+
if file_name.startswith(p['prefix']):
168+
if p['replacement'] is None:
169+
source_name = file_name[len(p['prefix'])::]
170+
else:
171+
source_name = p['replacement'] + file_name[len(p['prefix'])::]
172+
break
173+
sources.append(source_name)
174+
if collect_sources:
175+
try:
176+
with open(file_name, 'r') as infile:
177+
source_content = infile.read()
178+
sources_content.append(source_content)
179+
except:
180+
print('Failed to read source: %s' % file_name)
181+
sources_content.append(None)
182+
else:
183+
source_id = sources_map[file_name]
184+
185+
address_delta = address - last_address
186+
source_id_delta = source_id - last_source_id
187+
line_delta = line - last_line
188+
column_delta = column - last_column
189+
mappings.append(encode_vlq(address_delta) + encode_vlq(source_id_delta) + encode_vlq(line_delta) + encode_vlq(column_delta))
190+
last_address = address
191+
last_source_id = source_id
192+
last_line = line
193+
last_column = column
194+
return {'version': 3, 'names': [], 'sources': sources, 'sourcesContent': sources_content, 'mappings': ','.join(mappings)}
195+
196+
def main():
197+
args = parse_args()
198+
199+
wasm_input = args.wasm
200+
with open(wasm_input, 'rb') as infile:
201+
wasm = infile.read()
202+
203+
entries = read_dwarf_entries(wasm_input, args.llvm_dwarfdump)
204+
205+
code_section_offset = get_code_section_offset(wasm)
206+
207+
prefixes = []
208+
for p in args.prefix:
209+
if '=' in p:
210+
prefix, replacement = p.split('=')
211+
prefixes.append({'prefix': prefix, 'replacement': replacement})
212+
else:
213+
prefixes.append({'prefix': p, 'replacement': None})
214+
215+
print('Saving to %s' % args.output)
216+
map = build_sourcemap(entries, code_section_offset, prefixes, args.sources)
217+
with open(args.output, 'w') as outfile:
218+
json.dump(map, outfile)
219+
220+
if args.strip:
221+
wasm = strip_debug_sections(wasm)
222+
223+
if args.source_map_url:
224+
wasm = append_source_mapping(wasm, args.source_map_url)
225+
226+
if args.w:
227+
print('Saving wasm to %s' % args.w)
228+
with open(args.w, 'wb') as outfile:
229+
outfile.write(wasm)
230+
231+
print('Done.')
232+
233+
if __name__ == '__main__':
234+
sys.exit(main())

0 commit comments

Comments
 (0)