Home | History | Annotate | Download | only in symsrc
      1 #!/usr/bin/env python
      2 # Copyright (c) 2011 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 """Usage: <win-path-to-pdb.pdb>
      7 This tool will take a PDB on the command line, extract the source files that
      8 were used in building the PDB, query SVN for which repository and revision
      9 these files are at, and then finally write this information back into the PDB
     10 in a format that the debugging tools understand.  This allows for automatic
     11 source debugging, as all of the information is contained in the PDB, and the
     12 debugger can go out and fetch the source files via SVN.
     13 
     14 You most likely want to run these immediately after a build, since the source
     15 input files need to match the generated PDB, and we want the correct SVN
     16 revision information for the exact files that were used for the build.
     17 
     18 The following files from a windbg + source server installation are expected
     19 to reside in the same directory as this python script:
     20   dbghelp.dll
     21   pdbstr.exe
     22   srctool.exe
     23 
     24 NOTE: Expected to run under a native win32 python, NOT cygwin.  All paths are
     25 dealt with as win32 paths, since we have to interact with the Microsoft tools.
     26 """
     27 
     28 import sys
     29 import os
     30 import time
     31 import subprocess
     32 import tempfile
     33 
     34 # This serves two purposes.  First, it acts as a whitelist, and only files
     35 # from repositories listed here will be source indexed.  Second, it allows us
     36 # to map from one SVN URL to another, so we can map to external SVN servers.
     37 REPO_MAP = {
     38   "svn://chrome-svn/blink": "http://src.chromium.org/blink",
     39   "svn://chrome-svn/chrome": "http://src.chromium.org/chrome",
     40   "svn://chrome-svn/multivm": "http://src.chromium.org/multivm",
     41   "svn://chrome-svn/native_client": "http://src.chromium.org/native_client",
     42   "svn://chrome-svn.corp.google.com/blink": "http://src.chromium.org/blink",
     43   "svn://chrome-svn.corp.google.com/chrome": "http://src.chromium.org/chrome",
     44   "svn://chrome-svn.corp.google.com/multivm": "http://src.chromium.org/multivm",
     45   "svn://chrome-svn.corp.google.com/native_client":
     46       "http://src.chromium.org/native_client",
     47   "svn://svn-mirror.golo.chromium.org/blink": "http://src.chromium.org/blink",
     48   "svn://svn-mirror.golo.chromium.org/chrome": "http://src.chromium.org/chrome",
     49   "svn://svn-mirror.golo.chromium.org/multivm":
     50       "http://src.chromium.org/multivm",
     51   "svn://svn-mirror.golo.chromium.org/native_client":
     52       "http://src.chromium.org/native_client",
     53   "http://v8.googlecode.com/svn": None,
     54   "http://google-breakpad.googlecode.com/svn": None,
     55   "http://googletest.googlecode.com/svn": None,
     56   "http://open-vcdiff.googlecode.com/svn": None,
     57   "http://google-url.googlecode.com/svn": None,
     58 }
     59 
     60 
     61 def FindFile(filename):
     62   """Return the full windows path to a file in the same dir as this code."""
     63   thisdir = os.path.dirname(os.path.join(os.path.curdir, __file__))
     64   return os.path.abspath(os.path.join(thisdir, filename))
     65 
     66 
     67 def ExtractSourceFiles(pdb_filename):
     68   """Extract a list of local paths of the source files from a PDB."""
     69   srctool = subprocess.Popen([FindFile('srctool.exe'), '-r', pdb_filename],
     70                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
     71   filelist = srctool.stdout.read()
     72   res = srctool.wait()
     73   if res != 0 or filelist.startswith("srctool: "):
     74     raise "srctool failed: " + filelist
     75   return [x for x in filelist.split('\r\n') if len(x) != 0]
     76 
     77 
     78 def ReadSourceStream(pdb_filename):
     79   """Read the contents of the source information stream from a PDB."""
     80   srctool = subprocess.Popen([FindFile('pdbstr.exe'),
     81                               '-r', '-s:srcsrv',
     82                               '-p:%s' % pdb_filename],
     83                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
     84   data = srctool.stdout.read()
     85   res = srctool.wait()
     86 
     87   if (res != 0 and res != -1) or data.startswith("pdbstr: "):
     88     raise "pdbstr failed: " + data
     89   return data
     90 
     91 
     92 def WriteSourceStream(pdb_filename, data):
     93   """Write the contents of the source information stream to a PDB."""
     94   # Write out the data to a temporary filename that we can pass to pdbstr.
     95   (f, fname) = tempfile.mkstemp()
     96   f = os.fdopen(f, "wb")
     97   f.write(data)
     98   f.close()
     99 
    100   srctool = subprocess.Popen([FindFile('pdbstr.exe'),
    101                               '-w', '-s:srcsrv',
    102                               '-i:%s' % fname,
    103                               '-p:%s' % pdb_filename],
    104                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    105   data = srctool.stdout.read()
    106   res = srctool.wait()
    107 
    108   if (res != 0 and res != -1) or data.startswith("pdbstr: "):
    109     raise "pdbstr failed: " + data
    110 
    111   os.unlink(fname)
    112 
    113 
    114 # TODO for performance, we should probably work in directories instead of
    115 # files.  I'm scared of DEPS and generated files, so for now we query each
    116 # individual file, and don't make assumptions that all files in the same
    117 # directory are part of the same repository or at the same revision number.
    118 def ExtractSvnInfo(local_filename):
    119   """Calls svn info to extract the repository, path, and revision."""
    120   # We call svn.bat to make sure and get the depot tools SVN and not cygwin.
    121   srctool = subprocess.Popen(['svn.bat', 'info', local_filename],
    122                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    123   info = srctool.stdout.read()
    124   res = srctool.wait()
    125   if res != 0:
    126     return None
    127   # Hack up into a dictionary of the fields printed by svn info.
    128   vals = dict((y.split(': ', 2) for y in info.split('\r\n') if y))
    129 
    130   root = vals['Repository Root']
    131   if not vals['URL'].startswith(root):
    132     raise "URL is not inside of the repository root?!?"
    133   path = vals['URL'][len(root):]
    134   rev  = int(vals['Revision'])
    135 
    136   return [root, path, rev]
    137 
    138 
    139 def UpdatePDB(pdb_filename, verbose=False):
    140   """Update a pdb file with source information."""
    141   dir_blacklist = { }
    142   # TODO(deanm) look into "compressing" our output, by making use of vars
    143   # and other things, so we don't need to duplicate the repo path and revs.
    144   lines = [
    145     'SRCSRV: ini ------------------------------------------------',
    146     'VERSION=1',
    147     'INDEXVERSION=2',
    148     'VERCTRL=Subversion',
    149     'DATETIME=%s' % time.asctime(),
    150     'SRCSRV: variables ------------------------------------------',
    151     'SVN_EXTRACT_TARGET_DIR=%targ%\%fnbksl%(%var3%)\%var4%',
    152     'SVN_EXTRACT_TARGET=%svn_extract_target_dir%\%fnfile%(%var1%)',
    153     'SVN_EXTRACT_CMD=cmd /c mkdir "%svn_extract_target_dir%" && cmd /c svn cat "%var2%%var3%@%var4%" --non-interactive > "%svn_extract_target%"',
    154     'SRCSRVTRG=%SVN_extract_target%',
    155     'SRCSRVCMD=%SVN_extract_cmd%',
    156     'SRCSRV: source files ---------------------------------------',
    157   ]
    158 
    159   if ReadSourceStream(pdb_filename):
    160     raise "PDB already has source indexing information!"
    161 
    162   filelist = ExtractSourceFiles(pdb_filename)
    163   for filename in filelist:
    164     filedir = os.path.dirname(filename)
    165 
    166     if verbose:
    167       print "Processing: %s" % filename
    168     # This directory is blacklisted, either because it's not part of the SVN
    169     # repository, or from one we're not interested in indexing.
    170     if dir_blacklist.get(filedir, False):
    171       if verbose:
    172         print "  skipping, directory is blacklisted."
    173       continue
    174 
    175     info = ExtractSvnInfo(filename)
    176 
    177     # Skip the file if it's not under an svn repository.  To avoid constantly
    178     # querying SVN for files outside of SVN control (for example, the CRT
    179     # sources), check if the directory is outside of SVN and blacklist it.
    180     if not info:
    181       if not ExtractSvnInfo(filedir):
    182         dir_blacklist[filedir] = True
    183       if verbose:
    184         print "  skipping, file is not in an SVN repository"
    185       continue
    186 
    187     root = info[0]
    188     path = info[1]
    189     rev  = info[2]
    190 
    191     # Check if file was from a svn repository we don't know about, or don't
    192     # want to index.  Blacklist the entire directory.
    193     if not REPO_MAP.has_key(info[0]):
    194       if verbose:
    195         print "  skipping, file is from an unknown SVN repository %s" % root
    196       dir_blacklist[filedir] = True
    197       continue
    198 
    199     # We might want to map an internal repository URL to an external repository.
    200     if REPO_MAP[root]:
    201       root = REPO_MAP[root]
    202 
    203     lines.append('%s*%s*%s*%s' % (filename, root, path, rev))
    204     if verbose:
    205       print "  indexed file."
    206 
    207   lines.append('SRCSRV: end ------------------------------------------------')
    208 
    209   WriteSourceStream(pdb_filename, '\r\n'.join(lines))
    210 
    211 
    212 def main():
    213   if len(sys.argv) < 2 or len(sys.argv) > 3:
    214     print "usage: file.pdb [-v]"
    215     return 1
    216 
    217   verbose = False
    218   if len(sys.argv) == 3:
    219     verbose = (sys.argv[2] == '-v')
    220 
    221   UpdatePDB(sys.argv[1], verbose=verbose)
    222   return 0
    223 
    224 
    225 if __name__ == '__main__':
    226   sys.exit(main())
    227