#!/usr/bin/env python # Copyright (c) 2011 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Usage: This tool will take a PDB on the command line, extract the source files that were used in building the PDB, query SVN for which repository and revision these files are at, and then finally write this information back into the PDB in a format that the debugging tools understand. This allows for automatic source debugging, as all of the information is contained in the PDB, and the debugger can go out and fetch the source files via SVN. You most likely want to run these immediately after a build, since the source input files need to match the generated PDB, and we want the correct SVN revision information for the exact files that were used for the build. The following files from a windbg + source server installation are expected to reside in the same directory as this python script: dbghelp.dll pdbstr.exe srctool.exe NOTE: Expected to run under a native win32 python, NOT cygwin. All paths are dealt with as win32 paths, since we have to interact with the Microsoft tools. """ import sys import os import time import subprocess import tempfile # This serves two purposes. First, it acts as a whitelist, and only files # from repositories listed here will be source indexed. Second, it allows us # to map from one SVN URL to another, so we can map to external SVN servers. REPO_MAP = { "svn://chrome-svn/chrome": "http://src.chromium.org/svn", "svn://chrome-svn.corp.google.com/chrome": "http://src.chromium.org/svn", "http://v8.googlecode.com/svn": None, "http://google-breakpad.googlecode.com/svn": None, "http://googletest.googlecode.com/svn": None, "http://open-vcdiff.googlecode.com/svn": None, "http://google-url.googlecode.com/svn": None, } def FindFile(filename): """Return the full windows path to a file in the same dir as this code.""" thisdir = os.path.dirname(os.path.join(os.path.curdir, __file__)) return os.path.abspath(os.path.join(thisdir, filename)) def ExtractSourceFiles(pdb_filename): """Extract a list of local paths of the source files from a PDB.""" srctool = subprocess.Popen([FindFile('srctool.exe'), '-r', pdb_filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE) filelist = srctool.stdout.read() res = srctool.wait() if res != 0 or filelist.startswith("srctool: "): raise "srctool failed: " + filelist return [x for x in filelist.split('\r\n') if len(x) != 0] def ReadSourceStream(pdb_filename): """Read the contents of the source information stream from a PDB.""" srctool = subprocess.Popen([FindFile('pdbstr.exe'), '-r', '-s:srcsrv', '-p:%s' % pdb_filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE) data = srctool.stdout.read() res = srctool.wait() if (res != 0 and res != -1) or data.startswith("pdbstr: "): raise "pdbstr failed: " + data return data def WriteSourceStream(pdb_filename, data): """Write the contents of the source information stream to a PDB.""" # Write out the data to a temporary filename that we can pass to pdbstr. (f, fname) = tempfile.mkstemp() f = os.fdopen(f, "wb") f.write(data) f.close() srctool = subprocess.Popen([FindFile('pdbstr.exe'), '-w', '-s:srcsrv', '-i:%s' % fname, '-p:%s' % pdb_filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE) data = srctool.stdout.read() res = srctool.wait() if (res != 0 and res != -1) or data.startswith("pdbstr: "): raise "pdbstr failed: " + data os.unlink(fname) # TODO for performance, we should probably work in directories instead of # files. I'm scared of DEPS and generated files, so for now we query each # individual file, and don't make assumptions that all files in the same # directory are part of the same repository or at the same revision number. def ExtractSvnInfo(local_filename): """Calls svn info to extract the repository, path, and revision.""" # We call svn.bat to make sure and get the depot tools SVN and not cygwin. srctool = subprocess.Popen(['svn.bat', 'info', local_filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE) info = srctool.stdout.read() res = srctool.wait() if res != 0: return None # Hack up into a dictionary of the fields printed by svn info. vals = dict((y.split(': ', 2) for y in info.split('\r\n') if y)) root = vals['Repository Root'] if not vals['URL'].startswith(root): raise "URL is not inside of the repository root?!?" path = vals['URL'][len(root):] rev = int(vals['Revision']) return [root, path, rev] def UpdatePDB(pdb_filename, verbose=False): """Update a pdb file with source information.""" dir_blacklist = { } # TODO(deanm) look into "compressing" our output, by making use of vars # and other things, so we don't need to duplicate the repo path and revs. lines = [ 'SRCSRV: ini ------------------------------------------------', 'VERSION=1', 'INDEXVERSION=2', 'VERCTRL=Subversion', 'DATETIME=%s' % time.asctime(), 'SRCSRV: variables ------------------------------------------', 'SVN_EXTRACT_TARGET_DIR=%targ%\%fnbksl%(%var3%)\%var4%', 'SVN_EXTRACT_TARGET=%svn_extract_target_dir%\%fnfile%(%var1%)', 'SVN_EXTRACT_CMD=cmd /c mkdir "%svn_extract_target_dir%" && cmd /c svn cat "%var2%%var3%@%var4%" --non-interactive > "%svn_extract_target%"', 'SRCSRVTRG=%SVN_extract_target%', 'SRCSRVCMD=%SVN_extract_cmd%', 'SRCSRV: source files ---------------------------------------', ] if ReadSourceStream(pdb_filename): raise "PDB already has source indexing information!" filelist = ExtractSourceFiles(pdb_filename) for filename in filelist: filedir = os.path.dirname(filename) if verbose: print "Processing: %s" % filename # This directory is blacklisted, either because it's not part of the SVN # repository, or from one we're not interested in indexing. if dir_blacklist.get(filedir, False): if verbose: print " skipping, directory is blacklisted." continue info = ExtractSvnInfo(filename) # Skip the file if it's not under an svn repository. To avoid constantly # querying SVN for files outside of SVN control (for example, the CRT # sources), check if the directory is outside of SVN and blacklist it. if not info: if not ExtractSvnInfo(filedir): dir_blacklist[filedir] = True if verbose: print " skipping, file is not in an SVN repository" continue root = info[0] path = info[1] rev = info[2] # Check if file was from a svn repository we don't know about, or don't # want to index. Blacklist the entire directory. if not REPO_MAP.has_key(info[0]): if verbose: print " skipping, file is from an unknown SVN repository %s" % root dir_blacklist[filedir] = True continue # We might want to map an internal repository URL to an external repository. if REPO_MAP[root]: root = REPO_MAP[root] lines.append('%s*%s*%s*%s' % (filename, root, path, rev)) if verbose: print " indexed file." lines.append('SRCSRV: end ------------------------------------------------') WriteSourceStream(pdb_filename, '\r\n'.join(lines)) def main(): if len(sys.argv) < 2 or len(sys.argv) > 3: print "usage: file.pdb [-v]" return 1 verbose = False if len(sys.argv) == 3: verbose = (sys.argv[2] == '-v') UpdatePDB(sys.argv[1], verbose=verbose) return 0 if __name__ == '__main__': sys.exit(main())