[Session restore] Rename group name Enabled to Restore.
[chromium-blink-merge.git] / tools / win / split_link / split_link.py
blob791bc90fca58229b7420a36e7a503087716764ae
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."""
10 import _winreg
11 import ctypes
12 import os
13 import re
14 import shutil
15 import subprocess
16 import sys
17 import tempfile
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
25 # be output.
26 IGNORE_DATA = 0
29 def Log(message):
30 print 'split_link:', message
33 def GetFlagsAndInputs(argv):
34 """Parses the command line intended for link.exe and return the flags and
35 input files."""
36 rsp_expanded = []
37 for arg in argv:
38 if arg[0] == '@':
39 with open(arg[1:]) as rsp:
40 rsp_expanded.extend(rsp.read().splitlines())
41 else:
42 rsp_expanded.append(arg)
44 # Use CommandLineToArgvW so we match link.exe parsing.
45 try:
46 size = ctypes.c_int()
47 ptr = ctypes.windll.shell32.CommandLineToArgvW(
48 ctypes.create_unicode_buffer(' '.join(rsp_expanded)),
49 ctypes.byref(size))
50 ref = ctypes.c_wchar_p * size.value
51 raw = ref.from_address(ptr)
52 args = [arg for arg in raw]
53 finally:
54 ctypes.windll.kernel32.LocalFree(ptr)
56 inputs = []
57 flags = []
58 intermediate_manifest = ''
59 for arg in args:
60 lower_arg = arg.lower()
61 # We'll be replacing these ourselves.
62 if lower_arg.startswith('/out:'):
63 continue
64 if lower_arg.startswith('/manifestfile:'):
65 intermediate_manifest = arg[arg.index(':')+1:]
66 continue
67 if lower_arg.startswith('/pdb:'):
68 continue
69 if (not lower_arg.startswith('/') and
70 lower_arg.endswith(('.obj', '.lib', '.res'))):
71 inputs.append(arg)
72 else:
73 flags.append(arg)
75 return flags, inputs, intermediate_manifest
78 def GetRegistryValue(subkey):
79 try:
80 val = _winreg.QueryValue(_winreg.HKEY_CURRENT_USER,
81 'Software\\Chromium\\' + subkey)
82 if os.path.exists(val):
83 return val
84 except WindowsError:
85 pass
87 raise SystemExit("Couldn't read from registry")
90 def GetOriginalLinkerPath():
91 return GetRegistryValue('split_link_installed')
94 def GetMtPath():
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):
103 return -1
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):
107 return i
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."""
114 result = set()
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
117 # underscore.
118 unresolved_regexes = [
119 (re.compile(r' : error LNK2019: unresolved external symbol ".*" \((.*)\)'
120 r' referenced in function'),
121 False),
122 (re.compile(r' : error LNK2001: unresolved external symbol ".*" \((.*)\)$'),
123 False),
124 (re.compile(r' : error LNK2019: unresolved external symbol (.*)'
125 r' referenced in function '),
126 True),
127 (re.compile(r' : error LNK2001: unresolved external symbol (.*)$'),
128 True),
130 for line in output.splitlines():
131 line = line.strip()
132 for regex, strip_leading_underscore in unresolved_regexes:
133 mo = regex.search(line)
134 if mo:
135 if strip_leading_underscore:
136 result.add(mo.group(1)[1:])
137 else:
138 result.add(mo.group(1))
139 break
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)):
146 print output
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."""
159 if index == 0:
160 return "chrome.dll"
161 else:
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
209 a temp file."""
210 link_exe = GetOriginalLinkerPath()
211 temp = tempfile.NamedTemporaryFile(
212 prefix='split_link_', suffix='.obj', delete=False)
213 temp.close()
214 subprocess.check_call([
215 link_exe, '/lib', '/nologo', '/extract:' + obj, lib, '/out:' + temp.name])
216 return 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
224 buffer_size = 2048
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)."""
235 if export[0] != '?':
236 return False
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."""
245 deffiles = []
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):
253 if i == j:
254 continue
255 is_data = \
256 [' DATA' if IsDataDefinition(export) and not IGNORE_DATA else ''
257 for export in part]
258 print >> f, '\n'.join(' ' + export + data
259 for export, data in zip(part, is_data))
260 deffiles.append(deffile)
261 return deffiles
264 def BuildImportLibs(flags, inputs_by_part, deffiles):
265 """Runs the linker to generate an import library."""
266 import_libs = []
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,
271 '/DEF:%s' % deffile]
272 RunLinker(flags_with_implib_and_deffile, i, inputs, 'implib', None)
273 import_libs.append(libfile)
274 return import_libs
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."""
282 dlls = []
283 all_succeeded = True
284 new_externals = []
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)
291 if deffile:
292 flags = flags + ['/DEF:%s' % deffile, '/LTCG']
293 stdout, rc, output = RunLinker(
294 flags, i, inputs_with_implib, 'final', intermediate_manifest)
295 if rc != 0:
296 all_succeeded = False
297 new_externals.append(ParseOutExternals(stdout))
298 else:
299 new_externals.append([])
300 dlls.append(output)
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(
307 inputs,
308 num_parts,
309 description_parts,
310 description_all,
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)
320 match_count = 0
321 for obj in objs:
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)
326 if i == -1:
327 raise SystemExit(
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)
333 match_count += 1
334 if match_count == 0:
335 raise SystemExit(
336 '%s, %s matched a lib, but no objs' % (lib_spec, obj_spec))
337 return by_parts
340 def main():
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())
346 inputs_by_part = []
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
351 # .def file.
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(
356 inputs,
357 num_parts,
358 description_parts,
359 description['all'],
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'])
365 if i == -1:
366 for part in inputs_by_part:
367 part.append(input_file)
368 else:
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
381 data_exports = 0
382 for i in range(5):
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)
387 if ok:
388 break
389 data_exports = 0
390 for i, part in enumerate(unresolved_by_part):
391 for export in part:
392 if IsDataDefinition(export):
393 print 'part %d contains data export: %s (aka %s)' % (
394 i, Unmangle(export), export)
395 data_exports += 1
396 deffiles = GenerateDefFiles(unresolved_by_part)
397 import_libs = BuildImportLibs(flags, inputs_by_part, deffiles)
398 else:
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.')
403 return 1
405 mt_exe = GetMtPath()
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)
416 return 0
419 if __name__ == '__main__':
420 sys.exit(main())