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