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