~ubuntu-branches/ubuntu/quantal/enigmail/quantal-security

« back to all changes in this revision

Viewing changes to mozilla/build/automationutils.py

  • Committer: Package Import Robot
  • Author(s): Chris Coulson
  • Date: 2013-09-13 16:02:15 UTC
  • mfrom: (0.12.16)
  • Revision ID: package-import@ubuntu.com-20130913160215-u3g8nmwa0pdwagwc
Tags: 2:1.5.2-0ubuntu0.12.10.1
* New upstream release v1.5.2 for Thunderbird 24

* Build enigmail using a stripped down Thunderbird 17 build system, as it's
  now quite difficult to build the way we were doing previously, with the
  latest Firefox build system
* Add debian/patches/no_libxpcom.patch - Don't link against libxpcom, as it
  doesn't exist anymore (but exists in the build system)
* Add debian/patches/use_sdk.patch - Use the SDK version of xpt.py and
  friends
* Drop debian/patches/ipc-pipe_rename.diff (not needed anymore)
* Drop debian/patches/makefile_depth.diff (not needed anymore)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# This Source Code Form is subject to the terms of the Mozilla Public
 
3
# License, v. 2.0. If a copy of the MPL was not distributed with this
 
4
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
5
 
 
6
from __future__ import with_statement
 
7
import glob, logging, os, platform, shutil, subprocess, sys, tempfile, urllib2, zipfile
 
8
import re
 
9
from urlparse import urlparse
 
10
 
 
11
__all__ = [
 
12
  "ZipFileReader",
 
13
  "addCommonOptions",
 
14
  "checkForCrashes",
 
15
  "dumpLeakLog",
 
16
  "isURL",
 
17
  "processLeakLog",
 
18
  "getDebuggerInfo",
 
19
  "DEBUGGER_INFO",
 
20
  "replaceBackSlashes",
 
21
  "wrapCommand",
 
22
  ]
 
23
 
 
24
# Map of debugging programs to information about them, like default arguments
 
25
# and whether or not they are interactive.
 
26
DEBUGGER_INFO = {
 
27
  # gdb requires that you supply the '--args' flag in order to pass arguments
 
28
  # after the executable name to the executable.
 
29
  "gdb": {
 
30
    "interactive": True,
 
31
    "args": "-q --args"
 
32
  },
 
33
 
 
34
  # valgrind doesn't explain much about leaks unless you set the
 
35
  # '--leak-check=full' flag.
 
36
  "valgrind": {
 
37
    "interactive": False,
 
38
    "args": "--leak-check=full"
 
39
  }
 
40
}
 
41
 
 
42
class ZipFileReader(object):
 
43
  """
 
44
  Class to read zip files in Python 2.5 and later. Limited to only what we
 
45
  actually use.
 
46
  """
 
47
 
 
48
  def __init__(self, filename):
 
49
    self._zipfile = zipfile.ZipFile(filename, "r")
 
50
 
 
51
  def __del__(self):
 
52
    self._zipfile.close()
 
53
 
 
54
  def _getnormalizedpath(self, path):
 
55
    """
 
56
    Gets a normalized path from 'path' (or the current working directory if
 
57
    'path' is None). Also asserts that the path exists.
 
58
    """
 
59
    if path is None:
 
60
      path = os.curdir
 
61
    path = os.path.normpath(os.path.expanduser(path))
 
62
    assert os.path.isdir(path)
 
63
    return path
 
64
 
 
65
  def _extractname(self, name, path):
 
66
    """
 
67
    Extracts a file with the given name from the zip file to the given path.
 
68
    Also creates any directories needed along the way.
 
69
    """
 
70
    filename = os.path.normpath(os.path.join(path, name))
 
71
    if name.endswith("/"):
 
72
      os.makedirs(filename)
 
73
    else:
 
74
      path = os.path.split(filename)[0]
 
75
      if not os.path.isdir(path):
 
76
        os.makedirs(path)
 
77
      with open(filename, "wb") as dest:
 
78
        dest.write(self._zipfile.read(name))
 
79
 
 
80
  def namelist(self):
 
81
    return self._zipfile.namelist()
 
82
 
 
83
  def read(self, name):
 
84
    return self._zipfile.read(name)
 
85
 
 
86
  def extract(self, name, path = None):
 
87
    if hasattr(self._zipfile, "extract"):
 
88
      return self._zipfile.extract(name, path)
 
89
 
 
90
    # This will throw if name is not part of the zip file.
 
91
    self._zipfile.getinfo(name)
 
92
 
 
93
    self._extractname(name, self._getnormalizedpath(path))
 
94
 
 
95
  def extractall(self, path = None):
 
96
    if hasattr(self._zipfile, "extractall"):
 
97
      return self._zipfile.extractall(path)
 
98
 
 
99
    path = self._getnormalizedpath(path)
 
100
 
 
101
    for name in self._zipfile.namelist():
 
102
      self._extractname(name, path)
 
103
 
 
104
log = logging.getLogger()
 
105
 
 
106
def isURL(thing):
 
107
  """Return True if |thing| looks like a URL."""
 
108
  # We want to download URLs like http://... but not Windows paths like c:\...
 
109
  return len(urlparse(thing).scheme) >= 2
 
110
 
 
111
def addCommonOptions(parser, defaults={}):
 
112
  parser.add_option("--xre-path",
 
113
                    action = "store", type = "string", dest = "xrePath",
 
114
                    # individual scripts will set a sane default
 
115
                    default = None,
 
116
                    help = "absolute path to directory containing XRE (probably xulrunner)")
 
117
  if 'SYMBOLS_PATH' not in defaults:
 
118
    defaults['SYMBOLS_PATH'] = None
 
119
  parser.add_option("--symbols-path",
 
120
                    action = "store", type = "string", dest = "symbolsPath",
 
121
                    default = defaults['SYMBOLS_PATH'],
 
122
                    help = "absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols")
 
123
  parser.add_option("--debugger",
 
124
                    action = "store", dest = "debugger",
 
125
                    help = "use the given debugger to launch the application")
 
126
  parser.add_option("--debugger-args",
 
127
                    action = "store", dest = "debuggerArgs",
 
128
                    help = "pass the given args to the debugger _before_ "
 
129
                           "the application on the command line")
 
130
  parser.add_option("--debugger-interactive",
 
131
                    action = "store_true", dest = "debuggerInteractive",
 
132
                    help = "prevents the test harness from redirecting "
 
133
                        "stdout and stderr for interactive debuggers")
 
134
 
 
135
def checkForCrashes(dumpDir, symbolsPath, testName=None):
 
136
  stackwalkPath = os.environ.get('MINIDUMP_STACKWALK', None)
 
137
  # try to get the caller's filename if no test name is given
 
138
  if testName is None:
 
139
    try:
 
140
      testName = os.path.basename(sys._getframe(1).f_code.co_filename)
 
141
    except:
 
142
      testName = "unknown"
 
143
 
 
144
  # Check preconditions
 
145
  dumps = glob.glob(os.path.join(dumpDir, '*.dmp'))
 
146
  if len(dumps) == 0:
 
147
    return False
 
148
 
 
149
  try:
 
150
    removeSymbolsPath = False
 
151
 
 
152
    # If our symbols are at a remote URL, download them now
 
153
    if symbolsPath and isURL(symbolsPath):
 
154
      print "Downloading symbols from: " + symbolsPath
 
155
      removeSymbolsPath = True
 
156
      # Get the symbols and write them to a temporary zipfile
 
157
      data = urllib2.urlopen(symbolsPath)
 
158
      symbolsFile = tempfile.TemporaryFile()
 
159
      symbolsFile.write(data.read())
 
160
      # extract symbols to a temporary directory (which we'll delete after
 
161
      # processing all crashes)
 
162
      symbolsPath = tempfile.mkdtemp()
 
163
      zfile = ZipFileReader(symbolsFile)
 
164
      zfile.extractall(symbolsPath)
 
165
 
 
166
    for d in dumps:
 
167
      stackwalkOutput = []
 
168
      stackwalkOutput.append("Crash dump filename: " + d)
 
169
      topFrame = None
 
170
      if symbolsPath and stackwalkPath and os.path.exists(stackwalkPath):
 
171
        # run minidump stackwalk
 
172
        p = subprocess.Popen([stackwalkPath, d, symbolsPath],
 
173
                             stdout=subprocess.PIPE,
 
174
                             stderr=subprocess.PIPE)
 
175
        (out, err) = p.communicate()
 
176
        if len(out) > 3:
 
177
          # minidump_stackwalk is chatty, so ignore stderr when it succeeds.
 
178
          stackwalkOutput.append(out)
 
179
          # The top frame of the crash is always the line after "Thread N (crashed)"
 
180
          # Examples:
 
181
          #  0  libc.so + 0xa888
 
182
          #  0  libnss3.so!nssCertificate_Destroy [certificate.c : 102 + 0x0]
 
183
          #  0  mozjs.dll!js::GlobalObject::getDebuggers() [GlobalObject.cpp:89df18f9b6da : 580 + 0x0]
 
184
          #  0  libxul.so!void js::gc::MarkInternal<JSObject>(JSTracer*, JSObject**) [Marking.cpp : 92 + 0x28]
 
185
          lines = out.splitlines()
 
186
          for i, line in enumerate(lines):
 
187
            if "(crashed)" in line:
 
188
              match = re.search(r"^ 0  (?:.*!)?(?:void )?([^\[]+)", lines[i+1])
 
189
              if match:
 
190
                topFrame = "@ %s" % match.group(1).strip()
 
191
              break
 
192
        else:
 
193
          stackwalkOutput.append("stderr from minidump_stackwalk:")
 
194
          stackwalkOutput.append(err)
 
195
        if p.returncode != 0:
 
196
          stackwalkOutput.append("minidump_stackwalk exited with return code %d" % p.returncode)
 
197
      else:
 
198
        if not symbolsPath:
 
199
          stackwalkOutput.append("No symbols path given, can't process dump.")
 
200
        if not stackwalkPath:
 
201
          stackwalkOutput.append("MINIDUMP_STACKWALK not set, can't process dump.")
 
202
        elif stackwalkPath and not os.path.exists(stackwalkPath):
 
203
          stackwalkOutput.append("MINIDUMP_STACKWALK binary not found: %s" % stackwalkPath)
 
204
      if not topFrame:
 
205
        topFrame = "Unknown top frame"
 
206
      log.info("PROCESS-CRASH | %s | application crashed [%s]", testName, topFrame)
 
207
      print '\n'.join(stackwalkOutput)
 
208
      dumpSavePath = os.environ.get('MINIDUMP_SAVE_PATH', None)
 
209
      if dumpSavePath:
 
210
        shutil.move(d, dumpSavePath)
 
211
        print "Saved dump as %s" % os.path.join(dumpSavePath,
 
212
                                                os.path.basename(d))
 
213
      else:
 
214
        os.remove(d)
 
215
      extra = os.path.splitext(d)[0] + ".extra"
 
216
      if os.path.exists(extra):
 
217
        os.remove(extra)
 
218
  finally:
 
219
    if removeSymbolsPath:
 
220
      shutil.rmtree(symbolsPath)
 
221
 
 
222
  return True
 
223
 
 
224
def getFullPath(directory, path):
 
225
  "Get an absolute path relative to 'directory'."
 
226
  return os.path.normpath(os.path.join(directory, os.path.expanduser(path)))
 
227
 
 
228
def searchPath(directory, path):
 
229
  "Go one step beyond getFullPath and try the various folders in PATH"
 
230
  # Try looking in the current working directory first.
 
231
  newpath = getFullPath(directory, path)
 
232
  if os.path.isfile(newpath):
 
233
    return newpath
 
234
 
 
235
  # At this point we have to fail if a directory was given (to prevent cases
 
236
  # like './gdb' from matching '/usr/bin/./gdb').
 
237
  if not os.path.dirname(path):
 
238
    for dir in os.environ['PATH'].split(os.pathsep):
 
239
      newpath = os.path.join(dir, path)
 
240
      if os.path.isfile(newpath):
 
241
        return newpath
 
242
  return None
 
243
 
 
244
def getDebuggerInfo(directory, debugger, debuggerArgs, debuggerInteractive = False):
 
245
 
 
246
  debuggerInfo = None
 
247
 
 
248
  if debugger:
 
249
    debuggerPath = searchPath(directory, debugger)
 
250
    if not debuggerPath:
 
251
      print "Error: Path %s doesn't exist." % debugger
 
252
      sys.exit(1)
 
253
 
 
254
    debuggerName = os.path.basename(debuggerPath).lower()
 
255
 
 
256
    def getDebuggerInfo(type, default):
 
257
      if debuggerName in DEBUGGER_INFO and type in DEBUGGER_INFO[debuggerName]:
 
258
        return DEBUGGER_INFO[debuggerName][type]
 
259
      return default
 
260
 
 
261
    debuggerInfo = {
 
262
      "path": debuggerPath,
 
263
      "interactive" : getDebuggerInfo("interactive", False),
 
264
      "args": getDebuggerInfo("args", "").split()
 
265
    }
 
266
 
 
267
    if debuggerArgs:
 
268
      debuggerInfo["args"] = debuggerArgs.split()
 
269
    if debuggerInteractive:
 
270
      debuggerInfo["interactive"] = debuggerInteractive
 
271
 
 
272
  return debuggerInfo
 
273
 
 
274
 
 
275
def dumpLeakLog(leakLogFile, filter = False):
 
276
  """Process the leak log, without parsing it.
 
277
 
 
278
  Use this function if you want the raw log only.
 
279
  Use it preferably with the |XPCOM_MEM_LEAK_LOG| environment variable.
 
280
  """
 
281
 
 
282
  # Don't warn (nor "info") if the log file is not there.
 
283
  if not os.path.exists(leakLogFile):
 
284
    return
 
285
 
 
286
  with open(leakLogFile, "r") as leaks:
 
287
    leakReport = leaks.read()
 
288
 
 
289
  # Only |XPCOM_MEM_LEAK_LOG| reports can be actually filtered out.
 
290
  # Only check whether an actual leak was reported.
 
291
  if filter and not "0 TOTAL " in leakReport:
 
292
    return
 
293
 
 
294
  # Simply copy the log.
 
295
  log.info(leakReport.rstrip("\n"))
 
296
 
 
297
def processSingleLeakFile(leakLogFileName, processType, leakThreshold):
 
298
  """Process a single leak log.
 
299
  """
 
300
 
 
301
  #                  Per-Inst  Leaked      Total  Rem ...
 
302
  #   0 TOTAL              17     192  419115886    2 ...
 
303
  # 833 nsTimerImpl        60     120      24726    2 ...
 
304
  lineRe = re.compile(r"^\s*\d+\s+(?P<name>\S+)\s+"
 
305
                      r"(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s+"
 
306
                      r"-?\d+\s+(?P<numLeaked>-?\d+)")
 
307
 
 
308
  processString = ""
 
309
  if processType:
 
310
    # eg 'plugin'
 
311
    processString = " %s process:" % processType
 
312
 
 
313
  crashedOnPurpose = False
 
314
  totalBytesLeaked = None
 
315
  leakAnalysis = []
 
316
  leakedObjectNames = []
 
317
  with open(leakLogFileName, "r") as leaks:
 
318
    for line in leaks:
 
319
      if line.find("purposefully crash") > -1:
 
320
        crashedOnPurpose = True
 
321
      matches = lineRe.match(line)
 
322
      if not matches:
 
323
        # eg: the leak table header row
 
324
        log.info(line.rstrip())
 
325
        continue
 
326
      name = matches.group("name")
 
327
      size = int(matches.group("size"))
 
328
      bytesLeaked = int(matches.group("bytesLeaked"))
 
329
      numLeaked = int(matches.group("numLeaked"))
 
330
      # Output the raw line from the leak log table if it is the TOTAL row,
 
331
      # or is for an object row that has been leaked.
 
332
      if numLeaked != 0 or name == "TOTAL":
 
333
        log.info(line.rstrip())
 
334
      # Analyse the leak log, but output later or it will interrupt the leak table
 
335
      if name == "TOTAL":
 
336
        totalBytesLeaked = bytesLeaked
 
337
      if size < 0 or bytesLeaked < 0 or numLeaked < 0:
 
338
        leakAnalysis.append("TEST-UNEXPECTED-FAIL | leakcheck |%s negative leaks caught!"
 
339
                            % processString)
 
340
        continue
 
341
      if name != "TOTAL" and numLeaked != 0:
 
342
        leakedObjectNames.append(name)
 
343
        leakAnalysis.append("TEST-INFO | leakcheck |%s leaked %d %s (%s bytes)"
 
344
                            % (processString, numLeaked, name, bytesLeaked))
 
345
  log.info('\n'.join(leakAnalysis))
 
346
 
 
347
  if totalBytesLeaked is None:
 
348
    # We didn't see a line with name 'TOTAL'
 
349
    if crashedOnPurpose:
 
350
      log.info("TEST-INFO | leakcheck |%s deliberate crash and thus no leak log"
 
351
               % processString)
 
352
    else:
 
353
      # TODO: This should be a TEST-UNEXPECTED-FAIL, but was changed to a warning
 
354
      # due to too many intermittent failures (see bug 831223).
 
355
      log.info("WARNING | leakcheck |%s missing output line for total leaks!"
 
356
               % processString)
 
357
    return
 
358
 
 
359
  if totalBytesLeaked == 0:
 
360
    log.info("TEST-PASS | leakcheck |%s no leaks detected!" % processString)
 
361
    return
 
362
 
 
363
  # totalBytesLeaked was seen and is non-zero.
 
364
  if totalBytesLeaked > leakThreshold:
 
365
    # Fail the run if we're over the threshold (which defaults to 0)
 
366
    prefix = "TEST-UNEXPECTED-FAIL"
 
367
  else:
 
368
    prefix = "WARNING"
 
369
  # Create a comma delimited string of the first N leaked objects found,
 
370
  # to aid with bug summary matching in TBPL. Note: The order of the objects
 
371
  # had no significance (they're sorted alphabetically).
 
372
  maxSummaryObjects = 5
 
373
  leakedObjectSummary = ', '.join(leakedObjectNames[:maxSummaryObjects])
 
374
  if len(leakedObjectNames) > maxSummaryObjects:
 
375
    leakedObjectSummary += ', ...'
 
376
  log.info("%s | leakcheck |%s %d bytes leaked (%s)"
 
377
           % (prefix, processString, totalBytesLeaked, leakedObjectSummary))
 
378
 
 
379
def processLeakLog(leakLogFile, leakThreshold = 0):
 
380
  """Process the leak log, including separate leak logs created
 
381
  by child processes.
 
382
 
 
383
  Use this function if you want an additional PASS/FAIL summary.
 
384
  It must be used with the |XPCOM_MEM_BLOAT_LOG| environment variable.
 
385
  """
 
386
 
 
387
  if not os.path.exists(leakLogFile):
 
388
    log.info("WARNING | leakcheck | refcount logging is off, so leaks can't be detected!")
 
389
    return
 
390
 
 
391
  if leakThreshold != 0:
 
392
    log.info("TEST-INFO | leakcheck | threshold set at %d bytes" % leakThreshold)
 
393
 
 
394
  (leakLogFileDir, leakFileBase) = os.path.split(leakLogFile)
 
395
  fileNameRegExp = re.compile(r".*?_([a-z]*)_pid\d*$")
 
396
  if leakFileBase[-4:] == ".log":
 
397
    leakFileBase = leakFileBase[:-4]
 
398
    fileNameRegExp = re.compile(r".*?_([a-z]*)_pid\d*.log$")
 
399
 
 
400
  for fileName in os.listdir(leakLogFileDir):
 
401
    if fileName.find(leakFileBase) != -1:
 
402
      thisFile = os.path.join(leakLogFileDir, fileName)
 
403
      processType = None
 
404
      m = fileNameRegExp.search(fileName)
 
405
      if m:
 
406
        processType = m.group(1)
 
407
      processSingleLeakFile(thisFile, processType, leakThreshold)
 
408
 
 
409
def replaceBackSlashes(input):
 
410
  return input.replace('\\', '/')
 
411
 
 
412
def wrapCommand(cmd):
 
413
  """
 
414
  If running on OS X 10.5 or older, wrap |cmd| so that it will
 
415
  be executed as an i386 binary, in case it's a 32-bit/64-bit universal
 
416
  binary.
 
417
  """
 
418
  if platform.system() == "Darwin" and \
 
419
     hasattr(platform, 'mac_ver') and \
 
420
     platform.mac_ver()[0][:4] < '10.6':
 
421
    return ["arch", "-arch", "i386"] + cmd
 
422
  # otherwise just execute the command normally
 
423
  return cmd