1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
|
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Takes the same arguments as Windows link.exe, and a definition of libraries
to split into subcomponents. Does multiple passes of link.exe invocation to
determine exports between parts and generates .def and import libraries to
cause symbols to be available to other parts."""
import _winreg
import ctypes
import os
import re
import shutil
import subprocess
import sys
import tempfile
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# This can be set to ignore data exports. The resulting DLLs will probably not
# run, but at least they can be generated. The log of data exports will still
# be output.
IGNORE_DATA = 0
def Log(message):
print 'split_link:', message
def GetFlagsAndInputs(argv):
"""Parses the command line intended for link.exe and return the flags and
input files."""
rsp_expanded = []
for arg in argv:
if arg[0] == '@':
with open(arg[1:]) as rsp:
rsp_expanded.extend(rsp.read().splitlines())
else:
rsp_expanded.append(arg)
# Use CommandLineToArgvW so we match link.exe parsing.
try:
size = ctypes.c_int()
ptr = ctypes.windll.shell32.CommandLineToArgvW(
ctypes.create_unicode_buffer(' '.join(rsp_expanded)),
ctypes.byref(size))
ref = ctypes.c_wchar_p * size.value
raw = ref.from_address(ptr)
args = [arg for arg in raw]
finally:
ctypes.windll.kernel32.LocalFree(ptr)
inputs = []
flags = []
intermediate_manifest = ''
for arg in args:
lower_arg = arg.lower()
# We'll be replacing these ourselves.
if lower_arg.startswith('/out:'):
continue
if lower_arg.startswith('/manifestfile:'):
intermediate_manifest = arg[arg.index(':')+1:]
continue
if lower_arg.startswith('/pdb:'):
continue
if (not lower_arg.startswith('/') and
lower_arg.endswith(('.obj', '.lib', '.res'))):
inputs.append(arg)
else:
flags.append(arg)
return flags, inputs, intermediate_manifest
def GetRegistryValue(subkey):
try:
val = _winreg.QueryValue(_winreg.HKEY_CURRENT_USER,
'Software\\Chromium\\' + subkey)
if os.path.exists(val):
return val
except WindowsError:
pass
raise SystemExit("Couldn't read from registry")
def GetOriginalLinkerPath():
return GetRegistryValue('split_link_installed')
def GetMtPath():
return GetRegistryValue('split_link_mt_path')
def PartFor(input_file, description_parts, description_all):
"""Determines which part a given link input should be put into (or all)."""
# Check if it should go in all parts.
input_file = input_file.lower()
if any(re.search(spec, input_file) for spec in description_all):
return -1
# Or pick which particular one it belongs in.
for i, spec_list in enumerate(description_parts):
if any(re.search(spec, input_file) for spec in spec_list):
return i
raise ValueError("couldn't find location for %s" % input_file)
def ParseOutExternals(output):
"""Given the stdout of link.exe, parses the error messages to retrieve all
symbols that are unresolved."""
result = set()
# Styles of messages for unresolved externals, and a boolean to indicate
# whether the error message emits the symbols with or without a leading
# underscore.
unresolved_regexes = [
(re.compile(r' : error LNK2019: unresolved external symbol ".*" \((.*)\)'
r' referenced in function'),
False),
(re.compile(r' : error LNK2001: unresolved external symbol ".*" \((.*)\)$'),
False),
(re.compile(r' : error LNK2019: unresolved external symbol (.*)'
r' referenced in function '),
True),
(re.compile(r' : error LNK2001: unresolved external symbol (.*)$'),
True),
]
for line in output.splitlines():
line = line.strip()
for regex, strip_leading_underscore in unresolved_regexes:
mo = regex.search(line)
if mo:
if strip_leading_underscore:
result.add(mo.group(1)[1:])
else:
result.add(mo.group(1))
break
mo = re.search(r'fatal error LNK1120: (\d+) unresolved externals', output)
# Make sure we have the same number that the linker thinks we have.
if mo is None and result:
raise SystemExit(output)
if len(result) != int(mo.group(1)):
print output
print 'Expecting %d, got %d' % (int(mo.group(1)), len(result))
assert len(result) == int(mo.group(1))
return sorted(result)
def AsCommandLineArgs(items):
"""Intended for output to a response file. Quotes all arguments."""
return '\n'.join('"' + x + '"' for x in items)
def OutputNameForIndex(index):
"""Gets the final output DLL name, given a zero-based index."""
if index == 0:
return "chrome.dll"
else:
return 'chrome%d.dll' % index
def ManifestNameForIndex(index):
return OutputNameForIndex(index) + '.intermediate.manifest'
def PdbNameForIndex(index):
return OutputNameForIndex(index) + '.pdb'
def RunLinker(flags, index, inputs, phase, intermediate_manifest):
"""Invokes the linker and returns the stdout, returncode and target name."""
rspfile = 'part%d_%s.rsp' % (index, phase)
with open(rspfile, 'w') as f:
print >> f, AsCommandLineArgs(inputs)
print >> f, AsCommandLineArgs(flags)
output_name = OutputNameForIndex(index)
manifest_name = ManifestNameForIndex(index)
print >> f, '/ENTRY:ChromeEmptyEntry@12'
print >> f, '/OUT:' + output_name
print >> f, '/MANIFESTFILE:' + manifest_name
print >> f, '/PDB:' + PdbNameForIndex(index)
# Log('[[[\n' + open(rspfile).read() + '\n]]]')
link_exe = GetOriginalLinkerPath()
popen = subprocess.Popen([link_exe, '@' + rspfile], stdout=subprocess.PIPE)
stdout, _ = popen.communicate()
if index == 0 and popen.returncode == 0 and intermediate_manifest:
# Hack for ninja build. After the linker runs, it does some manifest
# things and expects there to be a file in this location. We just put it
# there so it's happy. This is a no-op.
if os.path.isdir(os.path.dirname(intermediate_manifest)):
shutil.copyfile(manifest_name, intermediate_manifest)
return stdout, popen.returncode, output_name
def GetLibObjList(lib):
"""Gets the list of object files contained in a .lib."""
link_exe = GetOriginalLinkerPath()
popen = subprocess.Popen(
[link_exe, '/lib', '/nologo', '/list', lib], stdout=subprocess.PIPE)
stdout, _ = popen.communicate()
return stdout.splitlines()
def ExtractObjFromLib(lib, obj):
"""Extracts a .obj file contained in a .lib file. Returns the absolute path
a temp file."""
link_exe = GetOriginalLinkerPath()
temp = tempfile.NamedTemporaryFile(
prefix='split_link_', suffix='.obj', delete=False)
temp.close()
subprocess.check_call([
link_exe, '/lib', '/nologo', '/extract:' + obj, lib, '/out:' + temp.name])
return temp.name
def Unmangle(export):
"Returns the human-presentable name of a mangled symbol."""
# Use dbghelp.dll to demangle the name.
# TODO(scottmg): Perhaps a simple cache? Seems pretty fast though.
UnDecorateSymbolName = ctypes.windll.dbghelp.UnDecorateSymbolName
buffer_size = 2048
output_string = ctypes.create_string_buffer(buffer_size)
if not UnDecorateSymbolName(
export, ctypes.byref(output_string), buffer_size, 0):
raise ctypes.WinError()
return output_string.value
def IsDataDefinition(export):
"""Determines if a given name is data rather than a function. Always returns
False for C-style (as opposed to C++-style names)."""
if export[0] != '?':
return False
# If it contains a '(' we assume it's a function.
return '(' not in Unmangle(export)
def GenerateDefFiles(unresolved_by_part):
"""Given a list of unresolved externals, generates a .def file that will
cause all those symbols to be exported."""
deffiles = []
Log('generating .def files')
for i, part in enumerate(unresolved_by_part):
deffile = 'part%d.def' % i
with open(deffile, 'w') as f:
print >> f, 'LIBRARY %s' % OutputNameForIndex(i)
print >> f, 'EXPORTS'
for j, part in enumerate(unresolved_by_part):
if i == j:
continue
is_data = \
[' DATA' if IsDataDefinition(export) and not IGNORE_DATA else ''
for export in part]
print >> f, '\n'.join(' ' + export + data
for export, data in zip(part, is_data))
deffiles.append(deffile)
return deffiles
def BuildImportLibs(flags, inputs_by_part, deffiles):
"""Runs the linker to generate an import library."""
import_libs = []
Log('building import libs')
for i, (inputs, deffile) in enumerate(zip(inputs_by_part, deffiles)):
libfile = 'part%d.lib' % i
flags_with_implib_and_deffile = flags + ['/IMPLIB:%s' % libfile,
'/DEF:%s' % deffile]
RunLinker(flags_with_implib_and_deffile, i, inputs, 'implib', None)
import_libs.append(libfile)
return import_libs
def AttemptLink(flags, inputs_by_part, unresolved_by_part, deffiles,
import_libs, intermediate_manifest):
"""Tries to run the linker for all parts using the current round of
generated import libs and .def files. If the link fails, updates the
unresolved externals list per part."""
dlls = []
all_succeeded = True
new_externals = []
Log('unresolveds now: %r' % [len(part) for part in unresolved_by_part])
for i, (inputs, deffile) in enumerate(zip(inputs_by_part, deffiles)):
Log('running link, part %d' % i)
others_implibs = import_libs[:]
others_implibs.pop(i)
inputs_with_implib = inputs + filter(lambda x: x, others_implibs)
if deffile:
flags = flags + ['/DEF:%s' % deffile, '/LTCG']
stdout, rc, output = RunLinker(
flags, i, inputs_with_implib, 'final', intermediate_manifest)
if rc != 0:
all_succeeded = False
new_externals.append(ParseOutExternals(stdout))
else:
new_externals.append([])
dlls.append(output)
combined_externals = [sorted(set(prev) | set(new))
for prev, new in zip(unresolved_by_part, new_externals)]
return all_succeeded, dlls, combined_externals
def ExtractSubObjsTargetedAtAll(
inputs,
num_parts,
description_parts,
description_all,
description_all_from_libs):
"""For (lib, obj) tuples in the all_from_libs section, extract the obj out of
the lib and added it to inputs. Returns a list of lists for which part the
extracted obj belongs in (which is whichever the .lib isn't in)."""
by_parts = [[] for _ in range(num_parts)]
for lib_spec, obj_spec in description_all_from_libs:
for input_file in inputs:
if re.search(lib_spec, input_file):
objs = GetLibObjList(input_file)
match_count = 0
for obj in objs:
if re.search(obj_spec, obj, re.I):
extracted_obj = ExtractObjFromLib(input_file, obj)
#Log('extracted %s (%s %s)' % (extracted_obj, input_file, obj))
i = PartFor(input_file, description_parts, description_all)
if i == -1:
raise SystemExit(
'%s is already in all parts, but matched '
'%s in all_from_libs' % (input_file, obj))
# See note in main().
assert num_parts == 2, "Can't handle > 2 dlls currently"
by_parts[1 - i].append(obj)
match_count += 1
if match_count == 0:
raise SystemExit(
'%s, %s matched a lib, but no objs' % (lib_spec, obj_spec))
return by_parts
def main():
flags, inputs, intermediate_manifest = GetFlagsAndInputs(sys.argv[1:])
partition_file = os.path.normpath(
os.path.join(BASE_DIR, '../../../build/split_link_partition.py'))
with open(partition_file) as partition:
description = eval(partition.read())
inputs_by_part = []
description_parts = description['parts']
# We currently assume that if a symbol isn't in dll 0, then it's in dll 1
# when generating def files. Otherwise, we'd need to do more complex things
# to figure out where each symbol actually is to assign it to the correct
# .def file.
num_parts = len(description_parts)
assert num_parts == 2, "Can't handle > 2 dlls currently"
description_parts.reverse()
objs_from_libs = ExtractSubObjsTargetedAtAll(
inputs,
num_parts,
description_parts,
description['all'],
description['all_from_libs'])
objs_from_libs.reverse()
inputs_by_part = [[] for _ in range(num_parts)]
for input_file in inputs:
i = PartFor(input_file, description_parts, description['all'])
if i == -1:
for part in inputs_by_part:
part.append(input_file)
else:
inputs_by_part[i].append(input_file)
inputs_by_part.reverse()
# Put the subobjs on to the main list.
for i, part in enumerate(objs_from_libs):
Log('%d sub .objs added to part %d' % (len(part), i))
inputs_by_part[i].extend(part)
unresolved_by_part = [[] for _ in range(num_parts)]
import_libs = [None] * num_parts
deffiles = [None] * num_parts
data_exports = 0
for i in range(5):
Log('--- starting pass %d' % i)
ok, dlls, unresolved_by_part = AttemptLink(
flags, inputs_by_part, unresolved_by_part, deffiles, import_libs,
intermediate_manifest)
if ok:
break
data_exports = 0
for i, part in enumerate(unresolved_by_part):
for export in part:
if IsDataDefinition(export):
print 'part %d contains data export: %s (aka %s)' % (
i, Unmangle(export), export)
data_exports += 1
deffiles = GenerateDefFiles(unresolved_by_part)
import_libs = BuildImportLibs(flags, inputs_by_part, deffiles)
else:
if data_exports and not IGNORE_DATA:
print '%d data exports found, see report above.' % data_exports
print('These cannot be exported, and must be either duplicated to the '
'target DLL (if constant), or wrapped in a function.')
return 1
mt_exe = GetMtPath()
for i, dll in enumerate(dlls):
Log('embedding manifest in %s' % dll)
args = [mt_exe, '-nologo', '-manifest']
args.append(ManifestNameForIndex(i))
args.append(description['manifest'])
args.append('-outputresource:%s;2' % dll)
subprocess.check_call(args)
Log('built %r' % dlls)
return 0
if __name__ == '__main__':
sys.exit(main())
|