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.
18 'scons_platform': 'x86-32',
22 'scons_platform': 'x86-64',
26 'scons_platform': 'arm',
31 def RunningOnBuildbot():
32 return os.environ.get('BUILDBOT_SLAVE_TYPE') is not None
35 def GetHostPlatform():
36 sys_platform = sys.platform.lower()
37 if sys_platform.startswith('linux'):
39 elif sys_platform in ('win', 'win32', 'windows', 'cygwin'):
41 elif sys_platform in ('darwin', 'mac'):
44 raise Exception('Can not determine the platform!')
47 def SetDefaultContextAttributes(context):
49 Set default values for the attributes needed by the SCons function, so that
50 SCons can be run without needing ParseStandardCommandLine
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'
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'] = []
68 # Windows-specific environment manipulation
69 def SetupWindowsEnvironment(context):
70 # Poke around looking for MSVC. We should do something more principled in
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!')
80 # The location of MSVC can differ depending on the version.
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'),
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):
94 # The break statement did not execute.
95 raise Exception('Cannot find MSVC!')
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'),
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 + '\\')
111 # This environment variable will SCons to print debug info while it searches
113 context.SetEnv('SCONS_MSCOMMON_DEBUG', '-')
115 # Needed for finding devenv.
116 context['msvc'] = msvc
118 SetupGyp(context, [])
121 def SetupGyp(context, extra_vars=[]):
122 context.SetEnv('GYP_GENERATORS', 'ninja')
123 if RunningOnBuildbot():
126 'gomadir=/b/build/goma',
130 context.SetEnv('GYP_DEFINES', ' '.join(
131 context['gyp_vars'] + goma_opts + extra_vars))
134 def SetupLinuxEnvironment(context):
135 SetupGyp(context, ['target_arch='+context['gyp_arch']])
138 def SetupMacEnvironment(context):
139 SetupGyp(context, ['target_arch='+context['gyp_arch']])
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')
148 def ParseStandardCommandLine(context):
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.
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,
169 help='Build and test for code coverage.')
170 parser.add_option('--validator', dest='validator', default=False,
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')
187 options, args = parser.parse_args()
190 parser.error('Expected 3 arguments: mode arch clib')
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)
197 if arch not in ARCH_MAP:
198 parser.error('Invalid arch %r' % arch)
200 if clib not in ('newlib', 'glibc', 'pnacl'):
201 parser.error('Invalid clib %r' % clib)
203 # TODO(ncbray) allow a command-line override
204 platform = GetHostPlatform()
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'] = {
218 'coverage': 'Debug'}[mode]
219 context['gyp_arch'] = ARCH_MAP[arch]['gyp_arch']
220 context['gyp_vars'] = []
222 context['gyp_vars'].append('clang=1')
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
246 for key, value in sorted(context.config.items()):
247 print '%s=%s' % (key, value)
250 def EnsureDirectoryExists(path):
252 Create a directory if it does not already exist.
253 Does not mask failures, but there really shouldn't be any.
255 if not os.path.exists(path):
259 def TryToCleanContents(path, file_name_filter=lambda fn: True):
261 Remove the contents of a directory without touching the directory itself.
262 Ignores all failures.
264 if os.path.exists(path):
265 for fn in os.listdir(path):
266 TryToCleanPath(os.path.join(path, fn), file_name_filter)
269 def TryToCleanPath(path, file_name_filter=lambda fn: True):
271 Removes a file or directory.
272 Ignores all failures.
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)
285 print 'Skipping %s' % path
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
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':
305 print "FAILED: %s %s" % (op.__name__, repr(args))
308 print "RETRY: %s %s" % (op.__name__, repr(args))
309 time.sleep(pow(2, count))
311 # Don't mask the exception.
317 def _RemoveDirectory(path):
318 print 'Removing %s' % path
319 if os.path.exists(path):
323 print ' Path does not exist, nothing to do.'
326 def RemoveDirectory(path):
328 Remove a directory if it exists.
329 Does not mask failures, although it does retry a few times on Windows.
331 Retry(_RemoveDirectory, path)
334 # This is a sanity check so Command can print out better error information.
335 def FileCanBeFound(name, paths):
337 if os.path.exists(name):
339 # Paths with directories are not resolved using the PATH variable.
340 if os.path.dirname(name):
343 for path in paths.split(os.pathsep):
344 full = os.path.join(path, name)
345 if os.path.exists(full):
350 def RemoveGypBuildDirectories():
351 # Remove all directories on all platforms. Overkill, but it allows for
352 # straight-line code.
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')
362 RemoveDirectory('../xcodebuild')
363 RemoveDirectory('../out')
364 RemoveDirectory('src/third_party/nacl_sdk/arm-newlib')
367 def RemoveSconsBuildDirectories():
368 RemoveDirectory('scons-out')
369 RemoveDirectory('breakpad-out')
372 # Execute a command using Python's subprocess module.
373 def Command(context, cmd, cwd=None):
374 print 'Running command: %s' % ' '.join(cmd)
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
384 env = context.MakeCommandEnv()
385 script_path = os.environ['PATH']
386 os.environ['PATH'] = env['PATH']
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.
394 if context['dry_run']:
397 retcode = subprocess.call(cmd, cwd=cwd, env=env)
399 # Provide a nicer failure message.
400 # If subprocess cannot find the executable, it will throw a cryptic
402 print 'Executable %r cannot be found.' % cmd[0]
405 os.environ['PATH'] = script_path
407 print 'Command return code: %d' % retcode
413 # A specialized version of CommandStep.
414 def SCons(context, mode=None, platform=None, parallel=False, browser_test=False,
416 python = sys.executable
417 if mode is None: mode = context['default_scons_mode']
418 if platform is None: platform = context['default_scons_platform']
420 jobs = context['max_jobs']
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'])
435 '--mode='+','.join(mode),
436 'platform='+platform,
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.
449 Command(context, cmd, cwd)
452 class StepFailed(Exception):
454 Thrown when the step has failed.
458 class StopBuild(Exception):
460 Thrown when the entire build should stop. This does not indicate a failure,
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.
473 def __init__(self, name, status, halt_on_fail=True):
476 if 'step_suffix' in status.context:
477 suffix = status.context['step_suffix']
480 self.name = name + suffix
481 self.halt_on_fail = halt_on_fail
482 self.step_failed = False
484 # Called on entry to a 'with' block.
487 print '@@@BUILD_STEP %s@@@' % self.name
488 self.status.ReportBegin(self.name)
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.
502 elif isinstance(exception, StepFailed):
505 print 'Halting build step because of failure.'
510 print 'The build step threw an exception...'
512 traceback.print_exception(type, exception, trace, file=sys.stdout)
516 self.status.ReportFail(self.name)
517 print '@@@STEP_FAILURE@@@'
518 if self.halt_on_fail:
520 print 'Entire build halted because %s failed.' % self.name
523 self.status.ReportPass(self.name)
525 # Suppress any exception that occurred.
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)
534 # Adds arbitrary text inside the build stage on the waterfall.
536 print '@@@STEP_TEXT@%s@@@' % (text)
539 class BuildStatus(object):
541 Keeps track of the overall status of the build.
544 def __init__(self, context):
545 self.context = context
546 self.ever_failed = False
549 def ReportBegin(self, name):
552 def ReportPass(self, name):
553 self.steps.append((name, 'passed'))
555 def ReportFail(self, name):
556 self.steps.append((name, 'failed'))
557 self.ever_failed = True
559 # Handy info when this script is run outside of the buildbot.
560 def DisplayBuildStatus(self):
562 for step, status in self.steps:
563 print '%-40s[%s]' % (step, status)
567 print 'Build failed.'
569 print 'Build succeeded.'
571 def ReturnValue(self):
572 return int(self.ever_failed)
575 class BuildContext(object):
577 Encapsulates the information needed for running a build command. This
578 includes environment variables and default arguments for SCons invocations.
581 # Only allow these attributes on objects of this type.
582 __slots__ = ['status', 'global_env', 'config']
585 # The contents of global_env override os.environ for any commands run via
588 # PATH is a special case. See: Command.
589 self.global_env['PATH'] = os.environ.get('PATH', '')
592 self['dry_run'] = False
594 # Emulate dictionary subscripting.
595 def __getitem__(self, key):
596 return self.config[key]
598 # Emulate dictionary subscripting.
599 def __setitem__(self, key, value):
600 self.config[key] = value
602 # Emulate dictionary membership test
603 def __contains__(self, key):
604 return key in self.config
607 return self.config['platform'] == 'win'
610 return self.config['platform'] == 'linux'
613 return self.config['platform'] == 'mac'
615 def GetEnv(self, name, default=None):
616 return self.global_env.get(name, default)
618 def SetEnv(self, name, value):
619 self.global_env[name] = str(value)
621 def MakeCommandEnv(self):
622 # The external environment is not sanitized.
624 # Arbitrary variables can be overridden.
625 e.update(self.global_env)
629 def RunBuild(script, status):
631 script(status, status.context)
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.'
647 # Display a summary of the build.
648 status.DisplayBuildStatus()
650 sys.exit(status.ReturnValue())