285aa409536458e2b82fb3e0a8f7c70cebdd69b5
[platform/framework/web/crosswalk.git] / src / native_client / buildbot / buildbot_lib.py
1 #!/usr/bin/python
2 # Copyright (c) 2012 The Native Client 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 # Enable 'with' statements in Python 2.5
7 from __future__ import with_statement
8
9 import optparse
10 import os.path
11 import shutil
12 import subprocess
13 import sys
14 import time
15 import traceback
16
17
18 ARCH_MAP = {
19     '32': {
20         'gyp_arch': 'ia32',
21         'scons_platform': 'x86-32',
22         },
23     '64': {
24         'gyp_arch': 'x64',
25         'scons_platform': 'x86-64',
26         },
27     'arm': {
28         'gyp_arch': 'arm',
29         'scons_platform': 'arm',
30         },
31     }
32
33
34 def GetHostPlatform():
35   sys_platform = sys.platform.lower()
36   if sys_platform.startswith('linux'):
37     return 'linux'
38   elif sys_platform in ('win', 'win32', 'windows', 'cygwin'):
39     return 'win'
40   elif sys_platform in ('darwin', 'mac'):
41     return 'mac'
42   else:
43     raise Exception('Can not determine the platform!')
44
45 def SetDefaultContextAttributes(context):
46   """
47   Set default values for the attributes needed by the SCons function, so that
48   SCons can be run without needing ParseStandardCommandLine
49   """
50   platform = GetHostPlatform()
51   context['platform'] = platform
52   context['mode'] = 'opt'
53   context['default_scons_mode'] = ['opt-host', 'nacl']
54   context['default_scons_platform'] = ('x86-64' if platform == 'win'
55                                        else 'x86-32')
56   context['clang'] = False
57   context['asan'] = False
58   context['pnacl'] = False
59   context['use_glibc'] = False
60   context['use_breakpad_tools'] = False
61   context['max_jobs'] = 8
62   context['scons_args'] = []
63
64 def ParseStandardCommandLine(context):
65   """
66   The standard buildbot scripts require 3 arguments to run.  The first
67   argument (dbg/opt) controls if the build is a debug or a release build.  The
68   second argument (32/64) controls the machine architecture being targeted.
69   The third argument (newlib/glibc) controls which c library we're using for
70   the nexes.  Different buildbots may have different sets of arguments.
71   """
72
73   parser = optparse.OptionParser()
74   parser.add_option('-n', '--dry-run', dest='dry_run', default=False,
75                     action='store_true', help='Do not execute any commands.')
76   parser.add_option('--inside-toolchain', dest='inside_toolchain',
77                     default=bool(os.environ.get('INSIDE_TOOLCHAIN')),
78                     action='store_true', help='Inside toolchain build.')
79   parser.add_option('--clang', dest='clang', default=False,
80                     action='store_true', help='Build trusted code with Clang.')
81   parser.add_option('--coverage', dest='coverage', default=False,
82                     action='store_true',
83                     help='Build and test for code coverage.')
84   parser.add_option('--validator', dest='validator', default=False,
85                     action='store_true',
86                     help='Only run validator regression test')
87   parser.add_option('--asan', dest='asan', default=False,
88                     action='store_true', help='Build trusted code with ASan.')
89   parser.add_option('--scons-args', dest='scons_args', default =[],
90                     action='append', help='Extra scons arguments.')
91   parser.add_option('--step-suffix', metavar='SUFFIX', default='',
92                     help='Append SUFFIX to buildbot step names.')
93   parser.add_option('--no-gyp', dest='no_gyp', default=False,
94                     action='store_true', help='Do not run the gyp build')
95   parser.add_option('--use-breakpad-tools', dest='use_breakpad_tools',
96                     default=False, action='store_true',
97                     help='Use breakpad tools for testing')
98
99   options, args = parser.parse_args()
100
101   if len(args) != 3:
102     parser.error('Expected 3 arguments: mode arch clib')
103
104   # script + 3 args == 4
105   mode, arch, clib = args
106   if mode not in ('dbg', 'opt', 'coverage'):
107     parser.error('Invalid mode %r' % mode)
108
109   if arch not in ARCH_MAP:
110     parser.error('Invalid arch %r' % arch)
111
112   if clib not in ('newlib', 'glibc', 'pnacl'):
113     parser.error('Invalid clib %r' % clib)
114
115   # TODO(ncbray) allow a command-line override
116   platform = GetHostPlatform()
117
118   context['platform'] = platform
119   context['mode'] = mode
120   context['arch'] = arch
121   # ASan is Clang, so set the flag to simplify other checks.
122   context['clang'] = options.clang or options.asan
123   context['validator'] = options.validator
124   context['asan'] = options.asan
125   # TODO(ncbray) turn derived values into methods.
126   context['gyp_mode'] = {
127       'opt': 'Release',
128       'dbg': 'Debug',
129       'coverage': 'Debug'}[mode]
130   context['gyp_arch'] = ARCH_MAP[arch]['gyp_arch']
131   context['gyp_vars'] = []
132   if context['clang']:
133     context['gyp_vars'].append('clang=1')
134   if context['asan']:
135     context['gyp_vars'].append('asan=1')
136   context['default_scons_platform'] = ARCH_MAP[arch]['scons_platform']
137   context['default_scons_mode'] = ['nacl']
138   # Only Linux can build trusted code on ARM.
139   # TODO(mcgrathr): clean this up somehow
140   if arch != 'arm' or platform == 'linux':
141     context['default_scons_mode'] += [mode + '-host']
142   context['use_glibc'] = clib == 'glibc'
143   context['pnacl'] = clib == 'pnacl'
144   context['max_jobs'] = 8
145   context['dry_run'] = options.dry_run
146   context['inside_toolchain'] = options.inside_toolchain
147   context['step_suffix'] = options.step_suffix
148   context['no_gyp'] = options.no_gyp
149   context['coverage'] = options.coverage
150   context['use_breakpad_tools'] = options.use_breakpad_tools
151   context['scons_args'] = options.scons_args
152   # Don't run gyp on coverage builds.
153   if context['coverage']:
154     context['no_gyp'] = True
155
156   for key, value in sorted(context.config.items()):
157     print '%s=%s' % (key, value)
158
159
160 def EnsureDirectoryExists(path):
161   """
162   Create a directory if it does not already exist.
163   Does not mask failures, but there really shouldn't be any.
164   """
165   if not os.path.exists(path):
166     os.makedirs(path)
167
168
169 def TryToCleanContents(path, file_name_filter=lambda fn: True):
170   """
171   Remove the contents of a directory without touching the directory itself.
172   Ignores all failures.
173   """
174   if os.path.exists(path):
175     for fn in os.listdir(path):
176       TryToCleanPath(os.path.join(path, fn), file_name_filter)
177
178
179 def TryToCleanPath(path, file_name_filter=lambda fn: True):
180   """
181   Removes a file or directory.
182   Ignores all failures.
183   """
184   if os.path.exists(path):
185     if file_name_filter(path):
186       print 'Trying to remove %s' % path
187       if os.path.isdir(path):
188         shutil.rmtree(path, ignore_errors=True)
189       else:
190         try:
191           os.remove(path)
192         except Exception:
193           pass
194     else:
195       print 'Skipping %s' % path
196
197
198 def Retry(op, *args):
199   # Windows seems to be prone to having commands that delete files or
200   # directories fail.  We currently do not have a complete understanding why,
201   # and as a workaround we simply retry the command a few times.
202   # It appears that file locks are hanging around longer than they should.  This
203   # may be a secondary effect of processes hanging around longer than they
204   # should.  This may be because when we kill a browser sel_ldr does not exit
205   # immediately, etc.
206   # Virus checkers can also accidently prevent files from being deleted, but
207   # that shouldn't be a problem on the bots.
208   if GetHostPlatform() == 'win':
209     count = 0
210     while True:
211       try:
212         op(*args)
213         break
214       except Exception:
215         print "FAILED: %s %s" % (op.__name__, repr(args))
216         count += 1
217         if count < 5:
218           print "RETRY: %s %s" % (op.__name__, repr(args))
219           time.sleep(pow(2, count))
220         else:
221           # Don't mask the exception.
222           raise
223   else:
224     op(*args)
225
226
227 def _RemoveDirectory(path):
228   print 'Removing %s' % path
229   if os.path.exists(path):
230     shutil.rmtree(path)
231     print '    Succeeded.'
232   else:
233     print '    Path does not exist, nothing to do.'
234
235
236 def RemoveDirectory(path):
237   """
238   Remove a directory if it exists.
239   Does not mask failures, although it does retry a few times on Windows.
240   """
241   Retry(_RemoveDirectory, path)
242
243
244 # This is a sanity check so Command can print out better error information.
245 def FileCanBeFound(name, paths):
246   # CWD
247   if os.path.exists(name):
248     return True
249   # Paths with directories are not resolved using the PATH variable.
250   if os.path.dirname(name):
251     return False
252   # In path
253   for path in paths.split(os.pathsep):
254     full = os.path.join(path, name)
255     if os.path.exists(full):
256       return True
257   return False
258
259
260 def RemoveGypBuildDirectories():
261   # Remove all directories on all platforms.  Overkill, but it allows for
262   # straight-line code.
263   # Windows
264   RemoveDirectory('build/Debug')
265   RemoveDirectory('build/Release')
266   RemoveDirectory('build/Debug-Win32')
267   RemoveDirectory('build/Release-Win32')
268   RemoveDirectory('build/Debug-x64')
269   RemoveDirectory('build/Release-x64')
270
271   # Linux and Mac
272   RemoveDirectory('../xcodebuild')
273   RemoveDirectory('../out')
274   RemoveDirectory('src/third_party/nacl_sdk/arm-newlib')
275
276
277 def RemoveSconsBuildDirectories():
278   RemoveDirectory('scons-out')
279   RemoveDirectory('breakpad-out')
280
281
282 # Execute a command using Python's subprocess module.
283 def Command(context, cmd, cwd=None):
284   print 'Running command: %s' % ' '.join(cmd)
285
286   # Python's subprocess has a quirk.  A subprocess can execute with an
287   # arbitrary, user-defined environment.  The first argument of the command,
288   # however, is located using the PATH variable of the Python script that is
289   # launching the subprocess.  Modifying the PATH in the environment passed to
290   # the subprocess does not affect Python's search for the first argument of
291   # the command (the executable file.)  This is a little counter intuitive,
292   # so we're forcing the search to use the same PATH variable as is seen by
293   # the subprocess.
294   env = context.MakeCommandEnv()
295   script_path = os.environ['PATH']
296   os.environ['PATH'] = env['PATH']
297
298   try:
299     if FileCanBeFound(cmd[0], env['PATH']) or context['dry_run']:
300       # Make sure that print statements before the subprocess call have been
301       # flushed, otherwise the output of the subprocess call may appear before
302       # the print statements.
303       sys.stdout.flush()
304       if context['dry_run']:
305         retcode = 0
306       else:
307         retcode = subprocess.call(cmd, cwd=cwd, env=env)
308     else:
309       # Provide a nicer failure message.
310       # If subprocess cannot find the executable, it will throw a cryptic
311       # exception.
312       print 'Executable %r cannot be found.' % cmd[0]
313       retcode = 1
314   finally:
315     os.environ['PATH'] = script_path
316
317   print 'Command return code: %d' % retcode
318   if retcode != 0:
319     raise StepFailed()
320   return retcode
321
322
323 # A specialized version of CommandStep.
324 def SCons(context, mode=None, platform=None, parallel=False, browser_test=False,
325           args=(), cwd=None):
326   python = sys.executable
327   if mode is None: mode = context['default_scons_mode']
328   if platform is None: platform = context['default_scons_platform']
329   if parallel:
330     jobs = context['max_jobs']
331   else:
332     jobs = 1
333   cmd = []
334   if browser_test and context.Linux():
335     # Although we could use the "browser_headless=1" Scons option, it runs
336     # xvfb-run once per Chromium invocation.  This is good for isolating
337     # the tests, but xvfb-run has a stupid fixed-period sleep, which would
338     # slow down the tests unnecessarily.
339     cmd.extend(['xvfb-run', '--auto-servernum'])
340   cmd.extend([
341       python, 'scons.py',
342       '--verbose',
343       '-k',
344       '-j%d' % jobs,
345       '--mode='+','.join(mode),
346       'platform='+platform,
347       ])
348   cmd.extend(context['scons_args'])
349   if context['clang']: cmd.append('--clang')
350   if context['asan']: cmd.append('--asan')
351   if context['use_glibc']: cmd.append('--nacl_glibc')
352   if context['pnacl']: cmd.append('bitcode=1')
353   if context['use_breakpad_tools']:
354     cmd.append('breakpad_tools_dir=breakpad-out')
355   # Append used-specified arguments.
356   cmd.extend(args)
357   Command(context, cmd, cwd)
358
359
360 class StepFailed(Exception):
361   """
362   Thrown when the step has failed.
363   """
364
365
366 class StopBuild(Exception):
367   """
368   Thrown when the entire build should stop.  This does not indicate a failure,
369   in of itself.
370   """
371
372
373 class Step(object):
374   """
375   This class is used in conjunction with a Python "with" statement to ensure
376   that the preamble and postamble of each build step gets printed and failures
377   get logged.  This class also ensures that exceptions thrown inside a "with"
378   statement don't take down the entire build.
379   """
380
381   def __init__(self, name, status, halt_on_fail=True):
382     self.status = status
383
384     if 'step_suffix' in status.context:
385       suffix = status.context['step_suffix']
386     else:
387       suffix = ''
388     self.name = name + suffix
389     self.halt_on_fail = halt_on_fail
390     self.step_failed = False
391
392   # Called on entry to a 'with' block.
393   def __enter__(self):
394     print
395     print '@@@BUILD_STEP %s@@@' % self.name
396     self.status.ReportBegin(self.name)
397
398   # The method is called on exit from a 'with' block - even for non-local
399   # control flow, i.e. exceptions, breaks, continues, returns, etc.
400   # If an exception is thrown inside a block wrapped with a 'with' statement,
401   # the __exit__ handler can suppress the exception by returning True.  This is
402   # used to isolate each step in the build - if an exception occurs in a given
403   # step, the step is treated as a failure.  This allows the postamble for each
404   # step to be printed and also allows the build to continue of the failure of
405   # a given step doesn't halt the build.
406   def __exit__(self, type, exception, trace):
407     if exception is None:
408       # If exception is None, no exception occurred.
409       step_failed = False
410     elif isinstance(exception, StepFailed):
411       step_failed = True
412       print
413       print 'Halting build step because of failure.'
414       print
415     else:
416       step_failed = True
417       print
418       print 'The build step threw an exception...'
419       print
420       traceback.print_exception(type, exception, trace, file=sys.stdout)
421       print
422
423     if step_failed:
424       self.status.ReportFail(self.name)
425       print '@@@STEP_FAILURE@@@'
426       if self.halt_on_fail:
427         print
428         print 'Entire build halted because %s failed.' % self.name
429         raise StopBuild()
430     else:
431       self.status.ReportPass(self.name)
432
433     # Suppress any exception that occurred.
434     return True
435
436
437 # Adds an arbitrary link inside the build stage on the waterfall.
438 def StepLink(text, link):
439   print '@@@STEP_LINK@%s@%s@@@' % (text, link)
440
441
442 # Adds arbitrary text inside the build stage on the waterfall.
443 def StepText(text):
444   print '@@@STEP_TEXT@%s@@@' % (text)
445
446
447 class BuildStatus(object):
448   """
449   Keeps track of the overall status of the build.
450   """
451
452   def __init__(self, context):
453     self.context = context
454     self.ever_failed = False
455     self.steps = []
456
457   def ReportBegin(self, name):
458     pass
459
460   def ReportPass(self, name):
461     self.steps.append((name, 'passed'))
462
463   def ReportFail(self, name):
464     self.steps.append((name, 'failed'))
465     self.ever_failed = True
466
467   # Handy info when this script is run outside of the buildbot.
468   def DisplayBuildStatus(self):
469     print
470     for step, status in self.steps:
471       print '%-40s[%s]' % (step, status)
472     print
473
474     if self.ever_failed:
475       print 'Build failed.'
476     else:
477       print 'Build succeeded.'
478
479   def ReturnValue(self):
480     return int(self.ever_failed)
481
482
483 class BuildContext(object):
484   """
485   Encapsulates the information needed for running a build command.  This
486   includes environment variables and default arguments for SCons invocations.
487   """
488
489   # Only allow these attributes on objects of this type.
490   __slots__ = ['status', 'global_env', 'config']
491
492   def __init__(self):
493     # The contents of global_env override os.environ for any commands run via
494     # self.Command(...)
495     self.global_env = {}
496     # PATH is a special case. See: Command.
497     self.global_env['PATH'] = os.environ.get('PATH', '')
498
499     self.config = {}
500     self['dry_run'] = False
501
502   # Emulate dictionary subscripting.
503   def __getitem__(self, key):
504     return self.config[key]
505
506   # Emulate dictionary subscripting.
507   def __setitem__(self, key, value):
508     self.config[key] = value
509
510   # Emulate dictionary membership test
511   def __contains__(self, key):
512     return key in self.config
513
514   def Windows(self):
515     return self.config['platform'] == 'win'
516
517   def Linux(self):
518     return self.config['platform'] == 'linux'
519
520   def Mac(self):
521     return self.config['platform'] == 'mac'
522
523   def GetEnv(self, name):
524     return self.global_env[name]
525
526   def SetEnv(self, name, value):
527     self.global_env[name] = str(value)
528
529   def MakeCommandEnv(self):
530     # The external environment is not sanitized.
531     e = dict(os.environ)
532     # Arbitrary variables can be overridden.
533     e.update(self.global_env)
534     return e
535
536
537 def RunBuild(script, status):
538   try:
539     script(status, status.context)
540   except StopBuild:
541     pass
542
543   # Emit a summary step for three reasons:
544   # - The annotator will attribute non-zero exit status to the last build step.
545   #   This can misattribute failures to the last build step.
546   # - runtest.py wraps the builds to scrape perf data. It emits an annotator
547   #   tag on exit which misattributes perf results to the last build step.
548   # - Provide a label step in which to show summary result.
549   #   Otherwise these go back to the preamble.
550   with Step('summary', status):
551     if status.ever_failed:
552       print 'There were failed stages.'
553     else:
554       print 'Success.'
555     # Display a summary of the build.
556     status.DisplayBuildStatus()
557
558   sys.exit(status.ReturnValue())