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 if options.verbose: 151 with print_lock: 152 print "Generating symbols for %s" % binary 153 154 syms = GetCommandOutput([GetDumpSymsBinary(options.build_dir), '-r', 155 binary]) 156 module_line = re.match("MODULE [^ ]+ [^ ]+ ([0-9A-F]+) (.*)\n", syms) 157 output_path = os.path.join(options.symbols_dir, module_line.group(2), 158 module_line.group(1)) 159 mkdir_p(output_path) 160 symbol_file = "%s.sym" % module_line.group(2) 161 f = open(os.path.join(output_path, symbol_file), 'w') 162 f.write(syms) 163 f.close() 164 165 queue.task_done() 166 167 for binary in binaries: 168 queue.put(binary) 169 170 for _ in range(options.jobs): 171 t = threading.Thread(target=_Worker) 172 t.daemon = True 173 t.start() 174 175 queue.join() 176 177 178def main(): 179 parser = optparse.OptionParser() 180 parser.add_option('', '--build-dir', default='', 181 help='The build output directory.') 182 parser.add_option('', '--symbols-dir', default='', 183 help='The directory where to write the symbols file.') 184 parser.add_option('', '--binary', default='', 185 help='The path of the binary to generate symbols for.') 186 parser.add_option('', '--clear', default=False, action='store_true', 187 help='Clear the symbols directory before writing new ' 188 'symbols.') 189 parser.add_option('-j', '--jobs', default=CONCURRENT_TASKS, action='store', 190 type='int', help='Number of parallel tasks to run.') 191 parser.add_option('-v', '--verbose', action='store_true', 192 help='Print verbose status output.') 193 194 (options, _) = parser.parse_args() 195 196 if not options.symbols_dir: 197 print "Required option --symbols-dir missing." 198 return 1 199 200 if not options.build_dir: 201 print "Required option --build-dir missing." 202 return 1 203 204 if not options.binary: 205 print "Required option --binary missing." 206 return 1 207 208 if not os.access(options.binary, os.X_OK): 209 print "Cannot find %s." % options.binary 210 return 1 211 212 if options.clear: 213 try: 214 shutil.rmtree(options.symbols_dir) 215 except: 216 pass 217 218 # Build the transitive closure of all dependencies. 219 binaries = set([options.binary]) 220 queue = [options.binary] 221 exe_path = os.path.dirname(options.binary) 222 while queue: 223 deps = GetSharedLibraryDependencies(options, queue.pop(0), exe_path) 224 new_deps = set(deps) - binaries 225 binaries |= new_deps 226 queue.extend(list(new_deps)) 227 228 GenerateSymbols(options, binaries) 229 230 return 0 231 232 233if '__main__' == __name__: 234 sys.exit(main()) 235