1#!/usr/bin/env python 2# Copyright 2013 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""A tool to generate symbols for a binary suitable for breakpad. 7 8Currently, the tool only supports Linux, Android, and Mac. Support for other 9platforms is planned. 10""" 11 12import errno 13import optparse 14import os 15import Queue 16import re 17import shutil 18import subprocess 19import sys 20import threading 21 22 23CONCURRENT_TASKS=4 24 25 26def GetCommandOutput(command): 27 """Runs the command list, returning its output. 28 29 Prints the given command (which should be a list of one or more strings), 30 then runs it and returns its output (stdout) as a string. 31 32 From chromium_utils. 33 """ 34 devnull = open(os.devnull, 'w') 35 proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=devnull, 36 bufsize=1) 37 output = proc.communicate()[0] 38 return output 39 40 41def GetDumpSymsBinary(build_dir=None): 42 """Returns the path to the dump_syms binary.""" 43 DUMP_SYMS = 'dump_syms' 44 dump_syms_bin = os.path.join(os.path.expanduser(build_dir), DUMP_SYMS) 45 if not os.access(dump_syms_bin, os.X_OK): 46 print 'Cannot find %s.' % DUMP_SYMS 47 sys.exit(1) 48 49 return dump_syms_bin 50 51 52def Resolve(path, exe_path, loader_path, rpaths): 53 """Resolve a dyld path. 54 55 @executable_path is replaced with |exe_path| 56 @loader_path is replaced with |loader_path| 57 @rpath is replaced with the first path in |rpaths| where the referenced file 58 is found 59 """ 60 path = path.replace('@loader_path', loader_path) 61 path = path.replace('@executable_path', exe_path) 62 if path.find('@rpath') != -1: 63 for rpath in rpaths: 64 new_path = Resolve(path.replace('@rpath', rpath), exe_path, loader_path, 65 []) 66 if os.access(new_path, os.X_OK): 67 return new_path 68 return '' 69 return path 70 71 72def GetSharedLibraryDependenciesLinux(binary): 73 """Return absolute paths to all shared library dependecies of the binary. 74 75 This implementation assumes that we're running on a Linux system.""" 76 ldd = GetCommandOutput(['ldd', binary]) 77 lib_re = re.compile('\t.* => (.+) \(.*\)$') 78 result = [] 79 for line in ldd.splitlines(): 80 m = lib_re.match(line) 81 if m: 82 result.append(m.group(1)) 83 return result 84 85 86def GetSharedLibraryDependenciesMac(binary, exe_path): 87 """Return absolute paths to all shared library dependecies of the binary. 88 89 This implementation assumes that we're running on a Mac system.""" 90 loader_path = os.path.dirname(binary) 91 otool = GetCommandOutput(['otool', '-l', binary]).splitlines() 92 rpaths = [] 93 for idx, line in enumerate(otool): 94 if line.find('cmd LC_RPATH') != -1: 95 m = re.match(' *path (.*) \(offset .*\)$', otool[idx+2]) 96 rpaths.append(m.group(1)) 97 98 otool = GetCommandOutput(['otool', '-L', binary]).splitlines() 99 lib_re = re.compile('\t(.*) \(compatibility .*\)$') 100 deps = [] 101 for line in otool: 102 m = lib_re.match(line) 103 if m: 104 dep = Resolve(m.group(1), exe_path, loader_path, rpaths) 105 if dep: 106 deps.append(os.path.normpath(dep)) 107 return deps 108 109 110def GetSharedLibraryDependencies(options, binary, exe_path): 111 """Return absolute paths to all shared library dependecies of the binary.""" 112 deps = [] 113 if sys.platform.startswith('linux'): 114 deps = GetSharedLibraryDependenciesLinux(binary) 115 elif sys.platform == 'darwin': 116 deps = GetSharedLibraryDependenciesMac(binary, exe_path) 117 else: 118 print "Platform not supported." 119 sys.exit(1) 120 121 result = [] 122 build_dir = os.path.abspath(options.build_dir) 123 for dep in deps: 124 if (os.access(dep, os.X_OK) and 125 os.path.abspath(os.path.dirname(dep)).startswith(build_dir)): 126 result.append(dep) 127 return result 128 129 130def mkdir_p(path): 131 """Simulates mkdir -p.""" 132 try: 133 os.makedirs(path) 134 except OSError as e: 135 if e.errno == errno.EEXIST and os.path.isdir(path): 136 pass 137 else: raise 138 139 140def GenerateSymbols(options, binaries): 141 """Dumps the symbols of binary and places them in the given directory.""" 142 143 queue = Queue.Queue() 144 print_lock = threading.Lock() 145 146 def _Worker(): 147 while True: 148 binary = queue.get() 149 150 should_dump_syms = True 151 reason = "no reason" 152 153 output_path = os.path.join( 154 options.symbols_dir, os.path.basename(binary)) 155 if os.path.isdir(output_path): 156 if os.path.getmtime(binary) < os.path.getmtime(output_path): 157 should_dump_syms = False 158 reason = "symbols are more current than binary" 159 160 if not should_dump_syms: 161 if options.verbose: 162 with print_lock: 163 print "Skipping %s (%s)" % (binary, reason) 164 queue.task_done() 165 continue 166 167 if options.verbose: 168 with print_lock: 169 print "Generating symbols for %s" % binary 170 171 if os.path.isdir(output_path): 172 os.utime(output_path, None) 173 174 syms = GetCommandOutput([GetDumpSymsBinary(options.build_dir), '-r', 175 binary]) 176 module_line = re.match("MODULE [^ ]+ [^ ]+ ([0-9A-F]+) (.*)\n", syms) 177 output_path = os.path.join(options.symbols_dir, module_line.group(2), 178 module_line.group(1)) 179 mkdir_p(output_path) 180 symbol_file = "%s.sym" % module_line.group(2) 181 try: 182 f = open(os.path.join(output_path, symbol_file), 'w') 183 f.write(syms) 184 f.close() 185 except Exception, e: 186 # Not much we can do about this. 187 with print_lock: 188 print e 189 190 queue.task_done() 191 192 for binary in binaries: 193 queue.put(binary) 194 195 for _ in range(options.jobs): 196 t = threading.Thread(target=_Worker) 197 t.daemon = True 198 t.start() 199 200 queue.join() 201 202 203def main(): 204 parser = optparse.OptionParser() 205 parser.add_option('', '--build-dir', default='', 206 help='The build output directory.') 207 parser.add_option('', '--symbols-dir', default='', 208 help='The directory where to write the symbols file.') 209 parser.add_option('', '--binary', default='', 210 help='The path of the binary to generate symbols for.') 211 parser.add_option('', '--clear', default=False, action='store_true', 212 help='Clear the symbols directory before writing new ' 213 'symbols.') 214 parser.add_option('-j', '--jobs', default=CONCURRENT_TASKS, action='store', 215 type='int', help='Number of parallel tasks to run.') 216 parser.add_option('-v', '--verbose', action='store_true', 217 help='Print verbose status output.') 218 219 (options, _) = parser.parse_args() 220 221 if not options.symbols_dir: 222 print "Required option --symbols-dir missing." 223 return 1 224 225 if not options.build_dir: 226 print "Required option --build-dir missing." 227 return 1 228 229 if not options.binary: 230 print "Required option --binary missing." 231 return 1 232 233 if not os.access(options.binary, os.X_OK): 234 print "Cannot find %s." % options.binary 235 return 1 236 237 if options.clear: 238 try: 239 shutil.rmtree(options.symbols_dir) 240 except: 241 pass 242 243 # Build the transitive closure of all dependencies. 244 binaries = set([options.binary]) 245 queue = [options.binary] 246 exe_path = os.path.dirname(options.binary) 247 while queue: 248 deps = GetSharedLibraryDependencies(options, queue.pop(0), exe_path) 249 new_deps = set(deps) - binaries 250 binaries |= new_deps 251 queue.extend(list(new_deps)) 252 253 GenerateSymbols(options, binaries) 254 255 return 0 256 257 258if '__main__' == __name__: 259 sys.exit(main()) 260