From c8960ff9ddcf63ca19063574a558ab7840b8c02a Mon Sep 17 00:00:00 2001 From: "bent.mozilla%gmail.com" Date: Wed, 26 Sep 2007 18:01:23 +0000 Subject: [PATCH] Bug 397436 - "Add SVN version information support for symbolstore.py". r=luser, a=bzbarsky. --- toolkit/crashreporter/tools/symbolstore.py | 275 ++++++++++++++++----- 1 file changed, 210 insertions(+), 65 deletions(-) diff --git a/toolkit/crashreporter/tools/symbolstore.py b/toolkit/crashreporter/tools/symbolstore.py index 94bc55c1ff5..52dd1fe8921 100755 --- a/toolkit/crashreporter/tools/symbolstore.py +++ b/toolkit/crashreporter/tools/symbolstore.py @@ -21,6 +21,7 @@ # # Contributor(s): # Ted Mielczarek +# Ben Turner # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or @@ -59,39 +60,177 @@ import re import shutil from optparse import OptionParser +# Utility classes + +class VCSFileInfo: + """ A base class for version-controlled file information. Ensures that the + following attributes are generated only once (successfully): + + self.root + self.revision + self.filename + + The attributes are generated by a single call to the GetRoot, + GetRevision, and GetFilename methods. Those methods are explicitly not + implemented here and must be implemented in derived classes. """ + + def __init__(self, file): + if not file: + raise ValueError + self.file = file + + def __getattr__(self, name): + """ __getattr__ is only called for attributes that are not set on self, + so setting self.[attr] will prevent future calls to the GetRoot, + GetRevision, and GetFilename methods. We don't set the values on + failure on the off chance that a future call might succeed. """ + + if name == "root": + root = self.GetRoot() + if root: + self.root = root + return root + + elif name == "revision": + revision = self.GetRevision() + if revision: + self.revision = revision + return revision + + elif name == "filename": + filename = self.GetFilename() + if filename: + self.filename = filename + return filename + + raise AttributeError + + def GetRoot(self): + """ This method should return the repository root for the file or 'None' + on failure. """ + raise NotImplementedError + + def GetRevision(self): + """ This method should return the revision number for the file or 'None' + on failure. """ + raise NotImplementedError + + def GetFilename(self): + """ This method should return the repository-specific filename for the + file or 'None' on failure. """ + raise NotImplementedError + +class CVSFileInfo(VCSFileInfo): + """ A class to maintiain version information for files in a CVS repository. + Derived from VCSFileInfo. """ + + def __init__(self, file, srcdir): + VCSFileInfo.__init__(self, file) + self.srcdir = srcdir + + def GetRoot(self): + (path, filename) = os.path.split(self.file) + root = os.path.join(path, "CVS", "Root") + if not os.path.isfile(root): + return None + f = open(root, "r") + root_name = f.readline().strip() + f.close() + parts = root_name.split("@") + if len(parts) > 1: + # we don't want the extra colon + return parts[1].replace(":","") + print >> sys.stderr, "Failed to get CVS Root for %s" % filename + return None + + def GetRevision(self): + (path, filename) = os.path.split(self.file) + entries = os.path.join(path, "CVS", "Entries") + if not os.path.isfile(entries): + return None + f = open(entries, "r") + for line in f: + parts = line.split("/") + if len(parts) > 1 and parts[1] == filename: + return parts[2] + print >> sys.stderr, "Failed to get CVS Revision for %s" % filename + return None + + def GetFilename(self): + file = self.file + if self.revision and self.root: + if self.srcdir: + # strip the base path off + # but we actually want the last dir in srcdir + file = os.path.normpath(file) + # the lower() is to handle win32+vc8, where + # the source filenames come out all lowercase, + # but the srcdir can be mixed case + if file.lower().startswith(self.srcdir.lower()): + file = file[len(self.srcdir):] + (head, tail) = os.path.split(self.srcdir) + if tail == "": + tail = os.path.basename(head) + file = tail + file + return "cvs:%s:%s:%s" % (self.root, file, self.revision) + return file + +class SVNFileInfo(VCSFileInfo): + url = None + repo = None + svndata = {} + + def __init__(self, file): + """ We only want to run subversion's info tool once so pull all the data + here. """ + + VCSFileInfo.__init__(self, file) + + if os.path.isfile(file): + _regex = re.compile(r'^(.+):\s(.+)$') + + command = os.popen("svn info %s" % file, "r") + for line in command: + match = _regex.match(line.strip()) + if match: + key = match.group(1) + if key in ["Repository Root", "Revision", "URL"]: + self.svndata[key] = match.group(2) + + exitStatus = command.close() + if exitStatus: + print >> sys.stderr, "Failed to get SVN info for %s" % file + + def GetRoot(self): + key = "Repository Root" + if key in self.svndata: + match = re.match(r'^\w+:\/+(\S+)', self.svndata[key]) + if match: + return match.group(1) + print >> sys.stderr, "Failed to get SVN Root for %s" % self.file + return None + + def GetRevision(self): + key = "Revision" + if key in self.svndata: + return self.svndata[key] + print >> sys.stderr, "Failed to get SVN Revision for %s" % self.file + return None + + def GetFilename(self): + if self.root and self.revision: + if "URL" in self.svndata and "Repository Root" in self.svndata: + url, repo = self.svndata["URL"], self.svndata["Repository Root"] + file = url[len(repo) + 1:] + return "svn:%s:%s:%s" % (self.root, file, self.revision) + print >> sys.stderr, "Failed to get SVN Filename for %s" % self.file + return self.file + # Utility functions -def GetCVSRevision(file): - """Given a full path to a file, look in CVS/Entries - for the CVS revision number""" - (path, filename) = os.path.split(file) - entries = os.path.join(path, "CVS", "Entries") - if not os.path.isfile(entries): - return None - f = open(entries, "r") - for line in f: - parts = line.split("/") - if len(parts) > 1 and parts[1] == filename: - return parts[2] - print >> sys.stderr, "Failed to get CVS Revision for %s" % filename - return None - -def GetCVSRoot(file): - """Given a full path to a file, look in CVS/Root - for the CVS Root""" - (path, filename) = os.path.split(file) - root = os.path.join(path, "CVS", "Root") - if not os.path.isfile(root): - return None - f = open(root, "r") - root_name = f.readline().strip() - f.close() - parts = root_name.split("@") - if len(parts) > 1: - # we don't want the extra colon - return parts[1].replace(":","") - print >> sys.stderr, "Failed to get CVS Root for %s" % filename - return None +# A cache of files for which VCS info has already been determined. Used to +# prevent extra filesystem activity or process launching. +vcsFileInfoCache = {} def GetVCSFilename(file, srcdir): """Given a full path to a file, and the top source directory, @@ -104,30 +243,24 @@ def GetVCSFilename(file, srcdir): (path, filename) = os.path.split(file) if path == '' or filename == '': return file - - cvsdir = os.path.join(path, "CVS") - if os.path.isdir(cvsdir): - rev = GetCVSRevision(file) - root = GetCVSRoot(file) - if rev is not None and root is not None: - if srcdir is not None: - # strip the base path off - # but we actually want the last dir in srcdir - file = os.path.normpath(file) - # the lower() is to handle win32+vc8, where - # the source filenames come out all lowercase, - # but the srcdir can be mixed case - if file.lower().startswith(srcdir.lower()): - file = file[len(srcdir):] - (head, tail) = os.path.split(srcdir) - if tail == "": - tail = os.path.basename(head) - file = tail + file - # we want forward slashes on win32 paths - file = file.replace("\\", "/") - return "cvs:%s:%s:%s" % (root, file, rev) - file = file.replace("\\", "/") - return file + + fileInfo = None + if file in vcsFileInfoCache: + # Already cached this info, use it. + fileInfo = vcsFileInfoCache[file] + else: + if os.path.isdir(os.path.join(path, "CVS")): + fileInfo = CVSFileInfo(file, srcdir) + elif os.path.isdir(os.path.join(path, ".svn")) or \ + os.path.isdir(os.path.join(path, "_svn")): + fileInfo = SVNFileInfo(file); + vcsFileInfoCache[file] = fileInfo + + if fileInfo: + file = fileInfo.filename + + # we want forward slashes on win32 paths + return file.replace("\\", "/") def GetPlatformSpecificDumper(**kwargs): """This function simply returns a instance of a subclass of Dumper @@ -194,7 +327,7 @@ class Dumper: return self.ProcessFile(file_or_dir) # maybe it doesn't exist? return False - + def ProcessDir(self, dir): """Process all the valid files in this directory. Valid files are determined by calling ShouldProcess.""" @@ -206,7 +339,7 @@ class Dumper: if not self.ProcessFile(fullpath): result = False return result - + def ProcessFile(self, file): """Dump symbols from this file into a symbol file, stored in the proper directory structure in |symbol_path|.""" @@ -269,6 +402,8 @@ class Dumper: # logic to determine what files to extract symbols from. class Dumper_Win32(Dumper): + fixedFilenameCaseCache = {} + def ShouldProcess(self, file): """This function will allow processing of pdb files that have dll or exe files with the same base name next to them.""" @@ -277,19 +412,29 @@ class Dumper_Win32(Dumper): if os.path.isfile(path + ".exe") or os.path.isfile(path + ".dll"): return True return False - + def FixFilenameCase(self, file): """Recent versions of Visual C++ put filenames into PDB files as all lowercase. If the file exists on the local filesystem, fix it.""" + + # Use a cached version if we have one. + if file in self.fixedFilenameCaseCache: + return self.fixedFilenameCaseCache[file] + + result = file + (path, filename) = os.path.split(file) - if not os.path.isdir(path): - return file - lc_filename = filename.lower() - for f in os.listdir(path): - if f.lower() == lc_filename: - return os.path.join(path, f) - return file + if os.path.isdir(path): + lc_filename = filename.lower() + for f in os.listdir(path): + if f.lower() == lc_filename: + result = os.path.join(path, f) + break + + # Cache the corrected version to avoid future filesystem hits. + self.fixedFilenameCaseCache[file] = result + return result class Dumper_Linux(Dumper): def ShouldProcess(self, file):