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',
30 'scons_platform': 'mips32',
35 def RunningOnBuildbot():
36 return os.environ.get('BUILDBOT_SLAVE_TYPE') is not None
39 def GetHostPlatform():
40 sys_platform = sys.platform.lower()
41 if sys_platform.startswith('linux'):
43 elif sys_platform in ('win', 'win32', 'windows', 'cygwin'):
45 elif sys_platform in ('darwin', 'mac'):
48 raise Exception('Can not determine the platform!')
51 def SetDefaultContextAttributes(context):
53 Set default values for the attributes needed by the SCons function, so that
54 SCons can be run without needing ParseStandardCommandLine
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'
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'] = []
72 # Windows-specific environment manipulation
73 def SetupWindowsEnvironment(context):
74 # Poke around looking for MSVC. We should do something more principled in
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!')
84 # The location of MSVC can differ depending on the version.
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'),
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):
98 # The break statement did not execute.
99 raise Exception('Cannot find MSVC!')
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'),
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 + '\\')
115 # This environment variable will SCons to print debug info while it searches
117 context.SetEnv('SCONS_MSCOMMON_DEBUG', '-')
119 # Needed for finding devenv.
120 context['msvc'] = msvc
122 SetupGyp(context, [])
125 def SetupGyp(context, extra_vars=[]):
126 context.SetEnv('GYP_GENERATORS', 'ninja')
127 if RunningOnBuildbot():
130 'gomadir=/b/build/goma',
134 context.SetEnv('GYP_DEFINES', ' '.join(
135 context['gyp_vars'] + goma_opts + extra_vars))
138 def SetupLinuxEnvironment(context):
139 SetupGyp(context, ['target_arch='+context['gyp_arch']])
142 def SetupMacEnvironment(context):
143 SetupGyp(context, ['target_arch='+context['gyp_arch']])
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')
152 def ParseStandardCommandLine(context):
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.
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,
173 help='Build and test for code coverage.')
174 parser.add_option('--validator', dest='validator', default=False,
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')
191 options, args = parser.parse_args()
194 parser.error('Expected 3 arguments: mode arch toolchain')
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)
201 if arch not in ARCH_MAP:
202 parser.error('Invalid arch %r' % arch)
204 if toolchain not in ('newlib', 'glibc', 'pnacl', 'nacl_clang'):
205 parser.error('Invalid toolchain %r' % toolchain)
207 # TODO(ncbray) allow a command-line override
208 platform = GetHostPlatform()
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'] = {
222 'coverage': 'Debug'}[mode]
223 context['gn_is_debug'] = {
226 'coverage': 'true'}[mode]
227 context['gyp_arch'] = ARCH_MAP[arch]['gyp_arch']
228 context['gyp_vars'] = []
230 context['gyp_vars'].append('clang=1')
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
255 for key, value in sorted(context.config.items()):
256 print '%s=%s' % (key, value)
259 def EnsureDirectoryExists(path):
261 Create a directory if it does not already exist.
262 Does not mask failures, but there really shouldn't be any.
264 if not os.path.exists(path):
268 def TryToCleanContents(path, file_name_filter=lambda fn: True):
270 Remove the contents of a directory without touching the directory itself.
271 Ignores all failures.
273 if os.path.exists(path):
274 for fn in os.listdir(path):
275 TryToCleanPath(os.path.join(path, fn), file_name_filter)
278 def TryToCleanPath(path, file_name_filter=lambda fn: True):
280 Removes a file or directory.
281 Ignores all failures.
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)
294 print 'Skipping %s' % path
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
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':
314 print "FAILED: %s %s" % (op.__name__, repr(args))
317 print "RETRY: %s %s" % (op.__name__, repr(args))
318 time.sleep(pow(2, count))
320 # Don't mask the exception.
326 def _RemoveDirectory(path):
327 print 'Removing %s' % path
328 if os.path.exists(path):
332 print ' Path does not exist, nothing to do.'
335 def RemoveDirectory(path):
337 Remove a directory if it exists.
338 Does not mask failures, although it does retry a few times on Windows.
340 Retry(_RemoveDirectory, path)
343 # This is a sanity check so Command can print out better error information.
344 def FileCanBeFound(name, paths):
346 if os.path.exists(name):
348 # Paths with directories are not resolved using the PATH variable.
349 if os.path.dirname(name):
352 for path in paths.split(os.pathsep):
353 full = os.path.join(path, name)
354 if os.path.exists(full):
359 def RemoveGypBuildDirectories():
360 # Remove all directories on all platforms. Overkill, but it allows for
361 # straight-line code.
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')
371 RemoveDirectory('../xcodebuild')
372 RemoveDirectory('../out')
373 RemoveDirectory('src/third_party/nacl_sdk/arm-newlib')
376 def RemoveSconsBuildDirectories():
377 RemoveDirectory('scons-out')
378 RemoveDirectory('breakpad-out')
381 # Execute a command using Python's subprocess module.
382 def Command(context, cmd, cwd=None):
383 print 'Running command: %s' % ' '.join(cmd)
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
393 env = context.MakeCommandEnv()
394 script_path = os.environ['PATH']
395 os.environ['PATH'] = env['PATH']
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.
403 if context['dry_run']:
406 retcode = subprocess.call(cmd, cwd=cwd, env=env)
408 # Provide a nicer failure message.
409 # If subprocess cannot find the executable, it will throw a cryptic
411 print 'Executable %r cannot be found.' % cmd[0]
414 os.environ['PATH'] = script_path
416 print 'Command return code: %d' % retcode
422 # A specialized version of CommandStep.
423 def SCons(context, mode=None, platform=None, parallel=False, browser_test=False,
425 python = sys.executable
426 if mode is None: mode = context['default_scons_mode']
427 if platform is None: platform = context['default_scons_platform']
429 jobs = context['max_jobs']
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'])
444 '--mode='+','.join(mode),
445 'platform='+platform,
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.
459 Command(context, cmd, cwd)
462 class StepFailed(Exception):
464 Thrown when the step has failed.
468 class StopBuild(Exception):
470 Thrown when the entire build should stop. This does not indicate a failure,
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.
483 def __init__(self, name, status, halt_on_fail=True):
486 if 'step_suffix' in status.context:
487 suffix = status.context['step_suffix']
490 self.name = name + suffix
491 self.halt_on_fail = halt_on_fail
492 self.step_failed = False
494 # Called on entry to a 'with' block.
497 print '@@@BUILD_STEP %s@@@' % self.name
498 self.status.ReportBegin(self.name)
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.
512 elif isinstance(exception, StepFailed):
515 print 'Halting build step because of failure.'
520 print 'The build step threw an exception...'
522 traceback.print_exception(type, exception, trace, file=sys.stdout)
526 self.status.ReportFail(self.name)
527 print '@@@STEP_FAILURE@@@'
528 if self.halt_on_fail:
530 print 'Entire build halted because %s failed.' % self.name
533 self.status.ReportPass(self.name)
535 # Suppress any exception that occurred.
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)
544 # Adds arbitrary text inside the build stage on the waterfall.
546 print '@@@STEP_TEXT@%s@@@' % (text)
549 class BuildStatus(object):
551 Keeps track of the overall status of the build.
554 def __init__(self, context):
555 self.context = context
556 self.ever_failed = False
559 def ReportBegin(self, name):
562 def ReportPass(self, name):
563 self.steps.append((name, 'passed'))
565 def ReportFail(self, name):
566 self.steps.append((name, 'failed'))
567 self.ever_failed = True
569 # Handy info when this script is run outside of the buildbot.
570 def DisplayBuildStatus(self):
572 for step, status in self.steps:
573 print '%-40s[%s]' % (step, status)
577 print 'Build failed.'
579 print 'Build succeeded.'
581 def ReturnValue(self):
582 return int(self.ever_failed)
585 class BuildContext(object):
587 Encapsulates the information needed for running a build command. This
588 includes environment variables and default arguments for SCons invocations.
591 # Only allow these attributes on objects of this type.
592 __slots__ = ['status', 'global_env', 'config']
595 # The contents of global_env override os.environ for any commands run via
598 # PATH is a special case. See: Command.
599 self.global_env['PATH'] = os.environ.get('PATH', '')
602 self['dry_run'] = False
604 # Emulate dictionary subscripting.
605 def __getitem__(self, key):
606 return self.config[key]
608 # Emulate dictionary subscripting.
609 def __setitem__(self, key, value):
610 self.config[key] = value
612 # Emulate dictionary membership test
613 def __contains__(self, key):
614 return key in self.config
617 return self.config['platform'] == 'win'
620 return self.config['platform'] == 'linux'
623 return self.config['platform'] == 'mac'
625 def GetEnv(self, name, default=None):
626 return self.global_env.get(name, default)
628 def SetEnv(self, name, value):
629 self.global_env[name] = str(value)
631 def MakeCommandEnv(self):
632 # The external environment is not sanitized.
634 # Arbitrary variables can be overridden.
635 e.update(self.global_env)
639 def RunBuild(script, status):
641 script(status, status.context)
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.'
657 # Display a summary of the build.
658 status.DisplayBuildStatus()
660 sys.exit(status.ReturnValue())