import argparse
import json
import logging
import re
import os
import platform
import posixpath
import shutil
import subprocess
import sys
from collections import Counter, namedtuple
from logging import info
from os import environ as env
from subprocess import Popen
from threading import Timer
Dirs = namedtuple('Dirs', ['scripts', 'js_src', 'source', 'tooltool'])
def directories(pathmodule, cwd, fixup=lambda s: s):
scripts = pathmodule.join(fixup(cwd), fixup(pathmodule.dirname(__file__)))
js_src = pathmodule.abspath(pathmodule.join(scripts, "..", ".."))
source = pathmodule.abspath(pathmodule.join(js_src, "..", ".."))
tooltool = pathmodule.abspath(env.get('TOOLTOOL_CHECKOUT',
pathmodule.join(source, "..", "..")))
return Dirs(scripts, js_src, source, tooltool)
DIR = directories(os.path, os.getcwd())
PDIR = directories(posixpath, os.environ["PWD"],
fixup=lambda s: re.sub(r'^(\w):', r'/\1', s))
env['CPP_UNIT_TESTS_DIR_JS_SRC'] = DIR.js_src
AUTOMATION = env.get('AUTOMATION', False)
parser = argparse.ArgumentParser(
description='Run a spidermonkey shell build job')
parser.add_argument('--verbose', action='store_true', default=AUTOMATION,
help="display additional logging info")
parser.add_argument('--dep', action='store_true',
help='do not clobber the objdir before building')
parser.add_argument('--keep', action='store_true',
help='do not delete the sanitizer output directory (for testing)')
parser.add_argument('--platform', '-p', type=str, metavar='PLATFORM',
default='', help='build platform, including a suffix ("-debug" or "") used '
'by buildbot to override the variant\'s "debug" setting. The platform can be '
'used to specify 32 vs 64 bits.')
parser.add_argument('--timeout', '-t', type=int, metavar='TIMEOUT',
default=10800,
help='kill job after TIMEOUT seconds')
parser.add_argument('--objdir', type=str, metavar='DIR',
default=env.get('OBJDIR', os.path.join(DIR.source, 'obj-spider')),
help='object directory')
group = parser.add_mutually_exclusive_group()
group.add_argument('--optimize', action='store_true',
help='generate an optimized build. Overrides variant setting.')
group.add_argument('--no-optimize', action='store_false',
dest='optimize',
help='generate a non-optimized build. Overrides variant setting.')
group.set_defaults(optimize=None)
group = parser.add_mutually_exclusive_group()
group.add_argument('--debug', action='store_true',
help='generate a debug build. Overrides variant setting.')
group.add_argument('--no-debug', action='store_false',
dest='debug',
help='generate a non-debug build. Overrides variant setting.')
group.set_defaults(debug=None)
group = parser.add_mutually_exclusive_group()
group.add_argument('--jemalloc', action='store_true',
dest='jemalloc',
help='use mozilla\'s jemalloc instead of the default allocator')
group.add_argument('--no-jemalloc', action='store_false',
dest='jemalloc',
help='use the default allocator instead of mozilla\'s jemalloc')
group.set_defaults(jemalloc=None)
parser.add_argument('--run-tests', '--tests', type=str, metavar='TESTSUITE',
default='',
help="comma-separated set of test suites to add to the variant's default set")
parser.add_argument('--skip-tests', '--skip', type=str, metavar='TESTSUITE',
default='',
help="comma-separated set of test suites to remove from the variant's default "
"set")
parser.add_argument('--build-only', '--build',
dest='skip_tests', action='store_const', const='all',
help="only do a build, do not run any tests")
parser.add_argument('--noconf', action='store_true',
help="skip running configure when doing a build")
parser.add_argument('--nobuild', action='store_true',
help='Do not do a build. Rerun tests on existing build.')
parser.add_argument('variant', type=str,
help='type of job requested, see variants/ subdir')
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format='%(message)s')
OBJDIR = args.objdir
OUTDIR = os.path.join(OBJDIR, "out")
POBJDIR = posixpath.join(PDIR.source, args.objdir)
MAKE = env.get('MAKE', 'make')
MAKEFLAGS = env.get('MAKEFLAGS', '-j6' + ('' if AUTOMATION else ' -s'))
for d in ('scripts', 'js_src', 'source', 'tooltool'):
info("DIR.{name} = {dir}".format(name=d, dir=getattr(DIR, d)))
def set_vars_from_script(script, vars):
script_text = 'source %s' % script
if platform.system() == 'Windows':
parse_state = 'parsing exports'
else:
script_text += '; echo VAR SETTINGS:; '
script_text += '; '.join('echo $' + var for var in vars)
parse_state = 'scanning'
stdout = subprocess.check_output(['sh', '-x', '-c', script_text])
tograb = vars[:]
for line in stdout.splitlines():
if parse_state == 'scanning':
if line == 'VAR SETTINGS:':
parse_state = 'grabbing'
elif parse_state == 'grabbing':
var = tograb.pop(0)
env[var] = line
elif parse_state == 'parsing exports':
m = re.match(r'export (\w+)=(.*)', line)
if m:
var, value = m.groups()
if var in tograb:
env[var] = value
info("Setting %s = %s" % (var, value))
def ensure_dir_exists(name, clobber=True, creation_marker_filename="CREATED-BY-AUTOSPIDER"):
if creation_marker_filename is None:
marker = None
else:
marker = os.path.join(name, creation_marker_filename)
if clobber:
if not AUTOMATION and marker and os.path.exists(name) and not os.path.exists(marker):
raise Exception(
"Refusing to delete objdir %s because it was not created by autospider" % name)
shutil.rmtree(name, ignore_errors=True)
try:
os.mkdir(name)
if marker:
open(marker, 'a').close()
except OSError:
if clobber:
raise
with open(os.path.join(DIR.scripts, "variants", args.variant)) as fh:
variant = json.load(fh)
if args.variant == 'nonunified':
for dirpath, dirnames, filenames in os.walk(DIR.js_src):
if 'moz.build' in filenames:
in_place = ['-i']
if platform.system() == 'Darwin':
in_place.append('')
subprocess.check_call(['sed'] + in_place + ['s/UNIFIED_SOURCES/SOURCES/',
os.path.join(dirpath, 'moz.build')])
CONFIGURE_ARGS = variant['configure-args']
opt = args.optimize
if opt is None:
opt = variant.get('optimize')
if opt is not None:
CONFIGURE_ARGS += (" --enable-optimize" if opt else " --disable-optimize")
opt = args.debug
if opt is None and args.platform:
opt = ('-debug' in args.platform)
if opt is None:
opt = variant.get('debug')
if opt is not None:
CONFIGURE_ARGS += (" --enable-debug" if opt else " --disable-debug")
opt = args.jemalloc
if opt is not None:
CONFIGURE_ARGS += (" --enable-jemalloc" if opt else " --disable-jemalloc")
word_bits = variant.get('bits')
if word_bits is None and args.platform:
platform_arch = args.platform.split('-')[0]
if platform_arch in ('win32', 'linux'):
word_bits = 32
elif platform_arch in ('win64', 'linux64'):
word_bits = 64
if word_bits is None:
word_bits = 64 if platform.architecture()[0] == '64bit' else 32
if 'compiler' in variant:
compiler = variant['compiler']
elif platform.system() == 'Darwin':
compiler = 'clang'
elif platform.system() == 'Windows':
compiler = 'cl'
else:
compiler = 'gcc'
info("using compiler '{}'".format(compiler))
cxx = {'clang': 'clang++', 'gcc': 'g++', 'cl': 'cl'}.get(compiler)
compiler_dir = env.get('GCCDIR', os.path.join(DIR.tooltool, compiler))
info("looking for compiler under {}/".format(compiler_dir))
if os.path.exists(os.path.join(compiler_dir, 'bin', compiler)):
env.setdefault('CC', os.path.join(compiler_dir, 'bin', compiler))
env.setdefault('CXX', os.path.join(compiler_dir, 'bin', cxx))
if compiler == 'clang':
platlib = 'lib'
else:
platlib = 'lib64' if word_bits == 64 else 'lib'
env.setdefault('LD_LIBRARY_PATH', os.path.join(compiler_dir, platlib))
else:
env.setdefault('CC', compiler)
env.setdefault('CXX', cxx)
bindir = os.path.join(OBJDIR, 'dist', 'bin')
env['LD_LIBRARY_PATH'] = ':'.join(
p for p in (bindir, env.get('LD_LIBRARY_PATH')) if p)
for v in ('CC', 'CXX', 'LD_LIBRARY_PATH'):
info("default {name} = {value}".format(name=v, value=env[v]))
rust_dir = os.path.join(DIR.tooltool, 'rustc')
if os.path.exists(os.path.join(rust_dir, 'bin', 'rustc')):
env.setdefault('RUSTC', os.path.join(rust_dir, 'bin', 'rustc'))
env.setdefault('CARGO', os.path.join(rust_dir, 'bin', 'cargo'))
else:
env.setdefault('RUSTC', 'rustc')
env.setdefault('CARGO', 'cargo')
if platform.system() == 'Darwin':
os.environ['SOURCE'] = DIR.source
set_vars_from_script(os.path.join(DIR.scripts, 'macbuildenv.sh'),
['CC', 'CXX'])
elif platform.system() == 'Windows':
MAKE = env.get('MAKE', 'mozmake')
os.environ['SOURCE'] = DIR.source
if word_bits == 64:
os.environ['USE_64BIT'] = '1'
set_vars_from_script(posixpath.join(PDIR.scripts, 'winbuildenv.sh'),
['PATH', 'VC_PATH', 'DIA_SDK_PATH', 'CC', 'CXX',
'WINDOWSSDKDIR'])
if word_bits == 32:
if platform.system() == 'Windows':
CONFIGURE_ARGS += ' --target=i686-pc-mingw32'
elif platform.system() == 'Linux':
if not platform.machine().startswith('arm'):
CONFIGURE_ARGS += ' --target=i686-pc-linux'
if not platform.machine().startswith('arm'):
if platform.system() == 'Windows':
sse_flags = '-arch:SSE2'
else:
sse_flags = '-msse -msse2 -mfpmath=sse'
env['CCFLAGS'] = '{0} {1}'.format(env.get('CCFLAGS', ''), sse_flags)
env['CXXFLAGS'] = '{0} {1}'.format(env.get('CXXFLAGS', ''), sse_flags)
else:
if platform.system() == 'Windows':
CONFIGURE_ARGS += ' --target=x86_64-pc-mingw32'
if platform.system() == 'Linux' and AUTOMATION:
CONFIGURE_ARGS = '--enable-stdcxx-compat --disable-gold ' + CONFIGURE_ARGS
ACTIVE_PROCESSES = set()
def killall():
for proc in ACTIVE_PROCESSES:
proc.kill()
ACTIVE_PROCESSES.clear()
timer = Timer(args.timeout, killall)
timer.daemon = True
timer.start()
ensure_dir_exists(OBJDIR, clobber=not args.dep and not args.nobuild)
ensure_dir_exists(OUTDIR, clobber=not args.keep)
env.setdefault('MOZ_UPLOAD_DIR', OBJDIR)
ensure_dir_exists(env['MOZ_UPLOAD_DIR'], clobber=False, creation_marker_filename=None)
info("MOZ_UPLOAD_DIR = {}".format(env['MOZ_UPLOAD_DIR']))
def run_command(command, check=False, **kwargs):
kwargs.setdefault('cwd', OBJDIR)
info("in directory {}, running {}".format(kwargs['cwd'], command))
proc = Popen(command, **kwargs)
ACTIVE_PROCESSES.add(proc)
stdout, stderr = None, None
try:
stdout, stderr = proc.communicate()
finally:
ACTIVE_PROCESSES.discard(proc)
status = proc.wait()
if check and status != 0:
raise subprocess.CalledProcessError(status, command, output=stderr)
return stdout, stderr, status
REPLACEMENTS = {
'DIR': DIR.scripts,
'TOOLTOOL_CHECKOUT': DIR.tooltool,
'MOZ_UPLOAD_DIR': env['MOZ_UPLOAD_DIR'],
'OUTDIR': OUTDIR,
}
for k, v in variant.get('env', {}).items():
env[k.encode('ascii')] = v.encode('ascii').format(**REPLACEMENTS)
if AUTOMATION:
if platform.system() == 'Linux' and platform.machine() == 'x86_64':
use_minidump = variant.get('use_minidump', True)
else:
use_minidump = False
else:
use_minidump = False
if use_minidump:
env.setdefault('MINIDUMP_SAVE_PATH', env['MOZ_UPLOAD_DIR'])
injector_lib = None
if platform.system() == 'Linux':
injector_lib = os.path.join(DIR.tooltool, 'breakpad-tools', 'libbreakpadinjector.so')
env.setdefault('MINIDUMP_STACKWALK',
os.path.join(DIR.tooltool, 'breakpad-tools', 'minidump_stackwalk'))
elif platform.system() == 'Darwin':
injector_lib = os.path.join(DIR.tooltool, 'breakpad-tools', 'breakpadinjector.dylib')
if not injector_lib or not os.path.exists(injector_lib):
use_minidump = False
info("use_minidump is {}".format(use_minidump))
info(" MINIDUMP_SAVE_PATH={}".format(env['MINIDUMP_SAVE_PATH']))
info(" injector lib is {}".format(injector_lib))
info(" MINIDUMP_STACKWALK={}".format(env.get('MINIDUMP_STACKWALK')))
def need_updating_configure(configure):
if not os.path.exists(configure):
return True
dep_files = [
os.path.join(DIR.js_src, 'configure.in'),
os.path.join(DIR.js_src, 'old-configure.in'),
]
for file in dep_files:
if os.path.getmtime(file) > os.path.getmtime(configure):
return True
return False
if not args.nobuild:
CONFIGURE_ARGS += ' --enable-nspr-build'
CONFIGURE_ARGS += ' --prefix={OBJDIR}/dist'.format(OBJDIR=POBJDIR)
configure = os.path.join(DIR.js_src, 'configure')
if need_updating_configure(configure):
shutil.copyfile(configure + ".in", configure)
os.chmod(configure, 0755)
if not args.noconf:
run_command(['sh', '-c', posixpath.join(PDIR.js_src, 'configure') + ' ' + CONFIGURE_ARGS],
check=True)
run_command('%s -w %s' % (MAKE, MAKEFLAGS), shell=True, check=True)
if use_minidump:
hostdir = os.path.join(OBJDIR, "dist", "host", "bin")
if not os.path.isdir(hostdir):
os.makedirs(hostdir)
shutil.copy(os.path.join(DIR.tooltool, "breakpad-tools", "dump_syms"),
os.path.join(hostdir, 'dump_syms'))
run_command([
'make',
'recurse_syms',
'MOZ_SOURCE_REPO=file://' + DIR.source,
'RUSTC_COMMIT=0',
'MOZ_CRASHREPORTER=1',
'MOZ_AUTOMATION_BUILD_SYMBOLS=1',
], check=True)
COMMAND_PREFIX = []
if subprocess.call("type setarch >/dev/null 2>&1", shell=True) == 0:
COMMAND_PREFIX.extend(['setarch', platform.machine(), '-R'])
def run_test_command(command, **kwargs):
_, _, status = run_command(COMMAND_PREFIX + command, check=False, **kwargs)
return status
test_suites = set(['jstests', 'jittest', 'jsapitests', 'checks'])
def normalize_tests(tests):
if 'all' in tests:
return test_suites
return tests
if args.platform:
variant_platform = args.platform.split("-")[0]
elif platform.system() == 'Windows':
variant_platform = 'win64' if word_bits == 64 else 'win32'
elif platform.system() == 'Linux':
variant_platform = 'linux64' if word_bits == 64 else 'linux'
elif platform.system() == 'Darwin':
variant_platform = 'macosx64'
else:
variant_platform = 'other'
for k, v in variant.get('conditional-env', {}).get(variant_platform, {}).items():
env[k.encode('ascii')] = v.encode('ascii').format(**REPLACEMENTS)
test_suites -= set(normalize_tests(variant.get('skip-tests', {}).get(variant_platform, [])))
test_suites -= set(normalize_tests(variant.get('skip-tests', {}).get('all', [])))
test_suites |= set(normalize_tests(variant.get('extra-tests', {}).get(variant_platform, [])))
test_suites |= set(normalize_tests(variant.get('extra-tests', {}).get('all', [])))
test_suites |= set(normalize_tests(args.run_tests.split(",")))
test_suites -= set(normalize_tests(args.skip_tests.split(",")))
if 'all' in args.skip_tests.split(","):
test_suites = []
if platform.system() == 'Windows':
env['JITTEST_EXTRA_ARGS'] = "-j1 " + env.get('JITTEST_EXTRA_ARGS', '')
if use_minidump:
for v in ('JSTESTS_EXTRA_ARGS', 'JITTEST_EXTRA_ARGS'):
env[v] = "--args='--dll %s' %s" % (injector_lib, env.get(v, ''))
results = []
if 'checks' in test_suites:
results.append(run_test_command([MAKE, 'check']))
if 'jittest' in test_suites:
results.append(run_test_command([MAKE, 'check-jit-test']))
if 'jsapitests' in test_suites:
jsapi_test_binary = os.path.join(OBJDIR, 'dist', 'bin', 'jsapi-tests')
test_env = env.copy()
if use_minidump and platform.system() == 'Linux':
test_env['LD_PRELOAD'] = injector_lib
st = run_test_command([jsapi_test_binary], env=test_env)
if st < 0:
print("PROCESS-CRASH | jsapi-tests | application crashed")
print("Return code: {}".format(st))
results.append(st)
if 'jstests' in test_suites:
results.append(run_test_command([MAKE, 'check-jstests']))
if variant.get('ignore-test-failures'):
logging.warning("Ignoring test results %s" % (results,))
results = [0]
if args.variant == 'msan':
files = filter(lambda f: f.startswith("sanitize_log."), os.listdir(OUTDIR))
fullfiles = [os.path.join(OUTDIR, f) for f in files]
sites = Counter()
errors = Counter()
for filename in fullfiles:
with open(os.path.join(OUTDIR, filename), 'rb') as fh:
for line in fh:
m = re.match(r'^SUMMARY: \w+Sanitizer: (?:data race|use-of-uninitialized-value) (.*)', line.strip())
if m:
site = re.sub(r'^(\S+?:\d+)(:\d+)* ', r'\1 ', m.group(1))
sites[site] += 1
summary_filename = os.path.join(env['MOZ_UPLOAD_DIR'], "%s_summary.txt" % args.variant)
with open(summary_filename, 'wb') as outfh:
for location, count in sites.most_common():
print >> outfh, "%d %s" % (count, location)
print(open(summary_filename, 'rb').read())
if 'max-errors' in variant:
max_allowed = variant['max-errors']
print("Found %d errors out of %d allowed" % (len(sites), max_allowed))
if len(sites) > max_allowed:
results.append(1)
command = ['tar', '-C', OUTDIR, '-zcf',
os.path.join(env['MOZ_UPLOAD_DIR'], '%s.tar.gz' % args.variant)]
command += files
subprocess.call(command)
if use_minidump:
venv_python = os.path.join(OBJDIR, "_virtualenvs", "init", "bin", "python")
run_command([
venv_python,
os.path.join(DIR.source, "testing/mozbase/mozcrash/mozcrash/mozcrash.py"),
os.getenv("TMPDIR", "/tmp"),
os.path.join(OBJDIR, "dist/crashreporter-symbols"),
])
for st in results:
if st != 0:
sys.exit(st)