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.
6 # Enable 'with' statements in Python 2.5
7 from __future__ import with_statement
21 'scons_platform': 'x86-32',
25 'scons_platform': 'x86-64',
29 'scons_platform': 'arm',
34 def GetHostPlatform():
35 sys_platform = sys.platform.lower()
36 if sys_platform.startswith('linux'):
38 elif sys_platform in ('win', 'win32', 'windows', 'cygwin'):
40 elif sys_platform in ('darwin', 'mac'):
43 raise Exception('Can not determine the platform!')
45 def SetDefaultContextAttributes(context):
47 Set default values for the attributes needed by the SCons function, so that
48 SCons can be run without needing ParseStandardCommandLine
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'
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'] = []
64 def ParseStandardCommandLine(context):
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.
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,
83 help='Build and test for code coverage.')
84 parser.add_option('--validator', dest='validator', default=False,
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')
99 options, args = parser.parse_args()
102 parser.error('Expected 3 arguments: mode arch clib')
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)
109 if arch not in ARCH_MAP:
110 parser.error('Invalid arch %r' % arch)
112 if clib not in ('newlib', 'glibc', 'pnacl'):
113 parser.error('Invalid clib %r' % clib)
115 # TODO(ncbray) allow a command-line override
116 platform = GetHostPlatform()
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'] = {
129 'coverage': 'Debug'}[mode]
130 context['gyp_arch'] = ARCH_MAP[arch]['gyp_arch']
131 context['gyp_vars'] = []
133 context['gyp_vars'].append('clang=1')
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
156 for key, value in sorted(context.config.items()):
157 print '%s=%s' % (key, value)
160 def EnsureDirectoryExists(path):
162 Create a directory if it does not already exist.
163 Does not mask failures, but there really shouldn't be any.
165 if not os.path.exists(path):
169 def TryToCleanContents(path, file_name_filter=lambda fn: True):
171 Remove the contents of a directory without touching the directory itself.
172 Ignores all failures.
174 if os.path.exists(path):
175 for fn in os.listdir(path):
176 TryToCleanPath(os.path.join(path, fn), file_name_filter)
179 def TryToCleanPath(path, file_name_filter=lambda fn: True):
181 Removes a file or directory.
182 Ignores all failures.
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)
195 print 'Skipping %s' % path
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
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':
215 print "FAILED: %s %s" % (op.__name__, repr(args))
218 print "RETRY: %s %s" % (op.__name__, repr(args))
219 time.sleep(pow(2, count))
221 # Don't mask the exception.
227 def _RemoveDirectory(path):
228 print 'Removing %s' % path
229 if os.path.exists(path):
233 print ' Path does not exist, nothing to do.'
236 def RemoveDirectory(path):
238 Remove a directory if it exists.
239 Does not mask failures, although it does retry a few times on Windows.
241 Retry(_RemoveDirectory, path)
244 # This is a sanity check so Command can print out better error information.
245 def FileCanBeFound(name, paths):
247 if os.path.exists(name):
249 # Paths with directories are not resolved using the PATH variable.
250 if os.path.dirname(name):
253 for path in paths.split(os.pathsep):
254 full = os.path.join(path, name)
255 if os.path.exists(full):
260 def RemoveGypBuildDirectories():
261 # Remove all directories on all platforms. Overkill, but it allows for
262 # straight-line code.
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')
272 RemoveDirectory('../xcodebuild')
273 RemoveDirectory('../out')
274 RemoveDirectory('src/third_party/nacl_sdk/arm-newlib')
277 def RemoveSconsBuildDirectories():
278 RemoveDirectory('scons-out')
279 RemoveDirectory('breakpad-out')
282 # Execute a command using Python's subprocess module.
283 def Command(context, cmd, cwd=None):
284 print 'Running command: %s' % ' '.join(cmd)
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
294 env = context.MakeCommandEnv()
295 script_path = os.environ['PATH']
296 os.environ['PATH'] = env['PATH']
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.
304 if context['dry_run']:
307 retcode = subprocess.call(cmd, cwd=cwd, env=env)
309 # Provide a nicer failure message.
310 # If subprocess cannot find the executable, it will throw a cryptic
312 print 'Executable %r cannot be found.' % cmd[0]
315 os.environ['PATH'] = script_path
317 print 'Command return code: %d' % retcode
323 # A specialized version of CommandStep.
324 def SCons(context, mode=None, platform=None, parallel=False, browser_test=False,
326 python = sys.executable
327 if mode is None: mode = context['default_scons_mode']
328 if platform is None: platform = context['default_scons_platform']
330 jobs = context['max_jobs']
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'])
345 '--mode='+','.join(mode),
346 'platform='+platform,
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.
357 Command(context, cmd, cwd)
360 class StepFailed(Exception):
362 Thrown when the step has failed.
366 class StopBuild(Exception):
368 Thrown when the entire build should stop. This does not indicate a failure,
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.
381 def __init__(self, name, status, halt_on_fail=True):
384 if 'step_suffix' in status.context:
385 suffix = status.context['step_suffix']
388 self.name = name + suffix
389 self.halt_on_fail = halt_on_fail
390 self.step_failed = False
392 # Called on entry to a 'with' block.
395 print '@@@BUILD_STEP %s@@@' % self.name
396 self.status.ReportBegin(self.name)
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.
410 elif isinstance(exception, StepFailed):
413 print 'Halting build step because of failure.'
418 print 'The build step threw an exception...'
420 traceback.print_exception(type, exception, trace, file=sys.stdout)
424 self.status.ReportFail(self.name)
425 print '@@@STEP_FAILURE@@@'
426 if self.halt_on_fail:
428 print 'Entire build halted because %s failed.' % self.name
431 self.status.ReportPass(self.name)
433 # Suppress any exception that occurred.
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)
442 # Adds arbitrary text inside the build stage on the waterfall.
444 print '@@@STEP_TEXT@%s@@@' % (text)
447 class BuildStatus(object):
449 Keeps track of the overall status of the build.
452 def __init__(self, context):
453 self.context = context
454 self.ever_failed = False
457 def ReportBegin(self, name):
460 def ReportPass(self, name):
461 self.steps.append((name, 'passed'))
463 def ReportFail(self, name):
464 self.steps.append((name, 'failed'))
465 self.ever_failed = True
467 # Handy info when this script is run outside of the buildbot.
468 def DisplayBuildStatus(self):
470 for step, status in self.steps:
471 print '%-40s[%s]' % (step, status)
475 print 'Build failed.'
477 print 'Build succeeded.'
479 def ReturnValue(self):
480 return int(self.ever_failed)
483 class BuildContext(object):
485 Encapsulates the information needed for running a build command. This
486 includes environment variables and default arguments for SCons invocations.
489 # Only allow these attributes on objects of this type.
490 __slots__ = ['status', 'global_env', 'config']
493 # The contents of global_env override os.environ for any commands run via
496 # PATH is a special case. See: Command.
497 self.global_env['PATH'] = os.environ.get('PATH', '')
500 self['dry_run'] = False
502 # Emulate dictionary subscripting.
503 def __getitem__(self, key):
504 return self.config[key]
506 # Emulate dictionary subscripting.
507 def __setitem__(self, key, value):
508 self.config[key] = value
510 # Emulate dictionary membership test
511 def __contains__(self, key):
512 return key in self.config
515 return self.config['platform'] == 'win'
518 return self.config['platform'] == 'linux'
521 return self.config['platform'] == 'mac'
523 def GetEnv(self, name):
524 return self.global_env[name]
526 def SetEnv(self, name, value):
527 self.global_env[name] = str(value)
529 def MakeCommandEnv(self):
530 # The external environment is not sanitized.
532 # Arbitrary variables can be overridden.
533 e.update(self.global_env)
537 def RunBuild(script, status):
539 script(status, status.context)
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.'
555 # Display a summary of the build.
556 status.DisplayBuildStatus()
558 sys.exit(status.ReturnValue())