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