Home | History | Annotate | Download | only in tools
      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 
      8 Currently, the tool only supports Linux, Android, and Mac. Support for other
      9 platforms is planned.
     10 """
     11 
     12 import errno
     13 import optparse
     14 import os
     15 import Queue
     16 import re
     17 import shutil
     18 import subprocess
     19 import sys
     20 import threading
     21 
     22 
     23 CONCURRENT_TASKS=4
     24 
     25 
     26 def 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 
     41 def 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 
     52 def 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 
     72 def 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 
     86 def 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 
    110 def 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 
    130 def 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 
    140 def 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 
    203 def 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 
    258 if '__main__' == __name__:
    259   sys.exit(main())
    260