1#!/usr/bin/env python3
2# This Source Code Form is subject to the terms of the Mozilla Public
3# License, v. 2.0. If a copy of the MPL was not distributed with this file,
4# You can obtain one at http://mozilla.org/MPL/2.0/.
5
6
7import argparse
8import json
9import logging
10import multiprocessing
11import re
12import os
13import platform
14import posixpath
15import shlex
16import shutil
17import subprocess
18import sys
19
20from collections import Counter, namedtuple
21from logging import info
22from os import environ as env
23from pathlib import Path
24from subprocess import Popen
25from threading import Timer
26
27Dirs = namedtuple("Dirs", ["scripts", "js_src", "source", "fetches"])
28
29
30def directories(pathmodule, cwd, fixup=lambda s: s):
31    scripts = pathmodule.join(fixup(cwd), fixup(pathmodule.dirname(__file__)))
32    js_src = pathmodule.abspath(pathmodule.join(scripts, "..", ".."))
33    source = pathmodule.abspath(pathmodule.join(js_src, "..", ".."))
34    mozbuild = pathmodule.abspath(
35        # os.path.expanduser does not work on Windows.
36        env.get("MOZBUILD_STATE_PATH")
37        or pathmodule.join(Path.home(), ".mozbuild")
38    )
39    fetches = pathmodule.abspath(env.get("MOZ_FETCHES_DIR", mozbuild))
40    return Dirs(scripts, js_src, source, fetches)
41
42
43def quote(s):
44    # shlex quotes for the purpose of passing to the native shell, which is cmd
45    # on Windows, and therefore will not replace backslashed paths with forward
46    # slashes. When such a path is passed to sh, the backslashes will be
47    # interpreted as escape sequences.
48    return shlex.quote(s).replace("\\", "/")
49
50
51# Some scripts will be called with sh, which cannot use backslashed
52# paths. So for direct subprocess.* invocation, use normal paths from
53# DIR, but when running under the shell, use POSIX style paths.
54DIR = directories(os.path, os.getcwd())
55PDIR = directories(
56    posixpath, os.environ["PWD"], fixup=lambda s: re.sub(r"^(\w):", r"/\1", s)
57)
58
59AUTOMATION = env.get("AUTOMATION", False)
60
61parser = argparse.ArgumentParser(description="Run a spidermonkey shell build job")
62parser.add_argument(
63    "--verbose",
64    action="store_true",
65    default=AUTOMATION,
66    help="display additional logging info",
67)
68parser.add_argument(
69    "--dep", action="store_true", help="do not clobber the objdir before building"
70)
71parser.add_argument(
72    "--keep",
73    action="store_true",
74    help="do not delete the sanitizer output directory (for testing)",
75)
76parser.add_argument(
77    "--platform",
78    "-p",
79    type=str,
80    metavar="PLATFORM",
81    default="",
82    help='build platform, including a suffix ("-debug" or "") used '
83    'by buildbot to override the variant\'s "debug" setting. The platform can be '
84    "used to specify 32 vs 64 bits.",
85)
86parser.add_argument(
87    "--timeout",
88    "-t",
89    type=int,
90    metavar="TIMEOUT",
91    default=12600,
92    help="kill job after TIMEOUT seconds",
93)
94parser.add_argument(
95    "--objdir",
96    type=str,
97    metavar="DIR",
98    # The real default must be set later so that OBJDIR and POBJDIR can be
99    # platform-dependent strings.
100    default=env.get("OBJDIR"),
101    help="object directory",
102)
103group = parser.add_mutually_exclusive_group()
104group.add_argument(
105    "--optimize",
106    action="store_true",
107    help="generate an optimized build. Overrides variant setting.",
108)
109group.add_argument(
110    "--no-optimize",
111    action="store_false",
112    dest="optimize",
113    help="generate a non-optimized build. Overrides variant setting.",
114)
115group.set_defaults(optimize=None)
116group = parser.add_mutually_exclusive_group()
117group.add_argument(
118    "--debug",
119    action="store_true",
120    help="generate a debug build. Overrides variant setting.",
121)
122group.add_argument(
123    "--no-debug",
124    action="store_false",
125    dest="debug",
126    help="generate a non-debug build. Overrides variant setting.",
127)
128group.set_defaults(debug=None)
129group = parser.add_mutually_exclusive_group()
130group.add_argument(
131    "--jemalloc",
132    action="store_true",
133    dest="jemalloc",
134    help="use mozilla's jemalloc instead of the default allocator",
135)
136group.add_argument(
137    "--no-jemalloc",
138    action="store_false",
139    dest="jemalloc",
140    help="use the default allocator instead of mozilla's jemalloc",
141)
142group.set_defaults(jemalloc=None)
143parser.add_argument(
144    "--run-tests",
145    "--tests",
146    type=str,
147    metavar="TESTSUITE",
148    default="",
149    help="comma-separated set of test suites to add to the variant's default set",
150)
151parser.add_argument(
152    "--skip-tests",
153    "--skip",
154    type=str,
155    metavar="TESTSUITE",
156    default="",
157    help="comma-separated set of test suites to remove from the variant's default "
158    "set",
159)
160parser.add_argument(
161    "--build-only",
162    "--build",
163    dest="skip_tests",
164    action="store_const",
165    const="all",
166    help="only do a build, do not run any tests",
167)
168parser.add_argument(
169    "--nobuild",
170    action="store_true",
171    help="Do not do a build. Rerun tests on existing build.",
172)
173parser.add_argument(
174    "variant", type=str, help="type of job requested, see variants/ subdir"
175)
176args = parser.parse_args()
177
178logging.basicConfig(level=logging.INFO, format="%(message)s")
179
180env["CPP_UNIT_TESTS_DIR_JS_SRC"] = DIR.js_src
181if AUTOMATION and platform.system() == "Windows":
182    # build/win{32,64}/mozconfig.vs-latest uses TOOLTOOL_DIR to set VSPATH.
183    env["TOOLTOOL_DIR"] = DIR.fetches
184
185OBJDIR = args.objdir or os.path.join(DIR.source, "obj-spider")
186OBJDIR = os.path.abspath(OBJDIR)
187OUTDIR = os.path.join(OBJDIR, "out")
188POBJDIR = args.objdir or posixpath.join(PDIR.source, "obj-spider")
189POBJDIR = posixpath.abspath(POBJDIR)
190MAKE = env.get("MAKE", "make")
191PYTHON = sys.executable
192
193for d in DIR._fields:
194    info("DIR.{name} = {dir}".format(name=d, dir=getattr(DIR, d)))
195
196
197def ensure_dir_exists(
198    name, clobber=True, creation_marker_filename="CREATED-BY-AUTOSPIDER"
199):
200    if creation_marker_filename is None:
201        marker = None
202    else:
203        marker = os.path.join(name, creation_marker_filename)
204    if clobber:
205        if (
206            not AUTOMATION
207            and marker
208            and os.path.exists(name)
209            and not os.path.exists(marker)
210        ):
211            raise Exception(
212                "Refusing to delete objdir %s because it was not created by autospider"
213                % name
214            )
215        shutil.rmtree(name, ignore_errors=True)
216    try:
217        os.mkdir(name)
218        if marker:
219            open(marker, "a").close()
220    except OSError:
221        if clobber:
222            raise
223
224
225with open(os.path.join(DIR.scripts, "variants", args.variant)) as fh:
226    variant = json.load(fh)
227
228if args.variant == "nonunified":
229    # Rewrite js/src/**/moz.build to replace UNIFIED_SOURCES to SOURCES.
230    # Note that this modifies the current checkout.
231    for dirpath, dirnames, filenames in os.walk(DIR.js_src):
232        if "moz.build" in filenames:
233            in_place = ["-i"]
234            if platform.system() == "Darwin":
235                in_place.append("")
236            subprocess.check_call(
237                ["sed"]
238                + in_place
239                + ["s/UNIFIED_SOURCES/SOURCES/", os.path.join(dirpath, "moz.build")]
240            )
241
242CONFIGURE_ARGS = variant["configure-args"]
243
244compiler = variant.get("compiler")
245if compiler != "gcc" and "clang-plugin" not in CONFIGURE_ARGS:
246    CONFIGURE_ARGS += " --enable-clang-plugin"
247
248if compiler == "gcc":
249    if AUTOMATION:
250        fetches = env["MOZ_FETCHES_DIR"]
251        env["CC"] = os.path.join(fetches, "gcc", "bin", "gcc")
252        env["CXX"] = os.path.join(fetches, "gcc", "bin", "g++")
253    else:
254        env["CC"] = "gcc"
255        env["CXX"] = "g++"
256
257opt = args.optimize
258if opt is None:
259    opt = variant.get("optimize")
260if opt is not None:
261    CONFIGURE_ARGS += " --enable-optimize" if opt else " --disable-optimize"
262
263opt = args.debug
264if opt is None:
265    opt = variant.get("debug")
266if opt is not None:
267    CONFIGURE_ARGS += " --enable-debug" if opt else " --disable-debug"
268
269opt = args.jemalloc
270if opt is not None:
271    CONFIGURE_ARGS += " --enable-jemalloc" if opt else " --disable-jemalloc"
272
273# By default, we build with NSPR, even if not specified. But we actively allow
274# builds to disable NSPR.
275opt = variant.get("nspr")
276if opt is None or opt:
277    CONFIGURE_ARGS += " --enable-nspr-build"
278
279# Some of the variants request a particular word size (eg ARM simulators).
280word_bits = variant.get("bits")
281
282# On Linux and Windows, we build 32- and 64-bit versions on a 64 bit
283# host, so the caller has to specify what is desired.
284if word_bits is None and args.platform:
285    platform_arch = args.platform.split("-")[0]
286    if platform_arch in ("win32", "linux"):
287        word_bits = 32
288    elif platform_arch in ("win64", "linux64"):
289        word_bits = 64
290
291# Fall back to the word size of the host.
292if word_bits is None:
293    word_bits = 64 if platform.architecture()[0] == "64bit" else 32
294
295# Need a platform name to use as a key in variant files.
296if args.platform:
297    variant_platform = args.platform.split("-")[0]
298elif platform.system() == "Windows":
299    variant_platform = "win64" if word_bits == 64 else "win32"
300elif platform.system() == "Linux":
301    variant_platform = "linux64" if word_bits == 64 else "linux"
302elif platform.system() == "Darwin":
303    variant_platform = "macosx64"
304else:
305    variant_platform = "other"
306
307env["LD_LIBRARY_PATH"] = ":".join(
308    d
309    for d in [
310        # for libnspr etc.
311        os.path.join(OBJDIR, "dist", "bin"),
312        # existing search path, if any
313        env.get("LD_LIBRARY_PATH"),
314    ]
315    if d is not None
316)
317
318os.environ["SOURCE"] = DIR.source
319if platform.system() == "Windows":
320    MAKE = env.get("MAKE", "mozmake")
321
322# Configure flags, based on word length and cross-compilation
323if word_bits == 32:
324    if platform.system() == "Windows":
325        CONFIGURE_ARGS += " --target=i686-pc-mingw32"
326    elif platform.system() == "Linux":
327        if not platform.machine().startswith("arm"):
328            CONFIGURE_ARGS += " --target=i686-pc-linux"
329
330    # Add SSE2 support for x86/x64 architectures.
331    if not platform.machine().startswith("arm"):
332        if platform.system() == "Windows":
333            sse_flags = "-arch:SSE2"
334        else:
335            sse_flags = "-msse -msse2 -mfpmath=sse"
336        env["CCFLAGS"] = "{0} {1}".format(env.get("CCFLAGS", ""), sse_flags)
337        env["CXXFLAGS"] = "{0} {1}".format(env.get("CXXFLAGS", ""), sse_flags)
338else:
339    if platform.system() == "Windows":
340        CONFIGURE_ARGS += " --target=x86_64-pc-mingw32"
341
342if platform.system() == "Linux" and AUTOMATION:
343    CONFIGURE_ARGS = "--enable-stdcxx-compat " + CONFIGURE_ARGS
344
345# Timeouts.
346ACTIVE_PROCESSES = set()
347
348
349def killall():
350    for proc in ACTIVE_PROCESSES:
351        proc.kill()
352    ACTIVE_PROCESSES.clear()
353
354
355timer = Timer(args.timeout, killall)
356timer.daemon = True
357timer.start()
358
359ensure_dir_exists(OBJDIR, clobber=not args.dep and not args.nobuild)
360ensure_dir_exists(OUTDIR, clobber=not args.keep)
361
362# Any jobs that wish to produce additional output can save them into the upload
363# directory if there is such a thing, falling back to OBJDIR.
364env.setdefault("MOZ_UPLOAD_DIR", OBJDIR)
365ensure_dir_exists(env["MOZ_UPLOAD_DIR"], clobber=False, creation_marker_filename=None)
366info("MOZ_UPLOAD_DIR = {}".format(env["MOZ_UPLOAD_DIR"]))
367
368
369def run_command(command, check=False, **kwargs):
370    kwargs.setdefault("cwd", OBJDIR)
371    info("in directory {}, running {}".format(kwargs["cwd"], command))
372    if platform.system() == "Windows":
373        # Windows will use cmd for the shell, which causes all sorts of
374        # problems. Use sh instead, quoting appropriately. (Use sh in all
375        # cases, not just when shell=True, because we want to be able to use
376        # paths that sh understands and cmd does not.)
377        if not isinstance(command, list):
378            if kwargs.get("shell"):
379                command = shlex.split(command)
380            else:
381                command = [command]
382
383        command = " ".join(quote(c) for c in command)
384        command = ["sh", "-c", command]
385        kwargs["shell"] = False
386    proc = Popen(command, **kwargs)
387    ACTIVE_PROCESSES.add(proc)
388    stdout, stderr = None, None
389    try:
390        stdout, stderr = proc.communicate()
391    finally:
392        ACTIVE_PROCESSES.discard(proc)
393    status = proc.wait()
394    if check and status != 0:
395        raise subprocess.CalledProcessError(status, command, output=stderr)
396    return stdout, stderr, status
397
398
399# Replacement strings in environment variables.
400REPLACEMENTS = {
401    "DIR": DIR.scripts,
402    "MOZ_FETCHES_DIR": DIR.fetches,
403    "MOZ_UPLOAD_DIR": env["MOZ_UPLOAD_DIR"],
404    "OUTDIR": OUTDIR,
405}
406
407# Add in environment variable settings for this variant. Normally used to
408# modify the flags passed to the shell or to set the GC zeal mode.
409for k, v in variant.get("env", {}).items():
410    env[k] = v.format(**REPLACEMENTS)
411
412if AUTOMATION:
413    # Currently only supported on linux64.
414    if platform.system() == "Linux" and word_bits == 64:
415        use_minidump = variant.get("use_minidump", True)
416    else:
417        use_minidump = False
418else:
419    use_minidump = False
420
421
422def resolve_path(dirs, *components):
423    if None in components:
424        return None
425    for dir in dirs:
426        path = os.path.join(dir, *components)
427        if os.path.exists(path):
428            return path
429
430
431if use_minidump:
432    env.setdefault("MINIDUMP_SAVE_PATH", env["MOZ_UPLOAD_DIR"])
433
434    injector_basename = {
435        "Linux": "libbreakpadinjector.so",
436        "Darwin": "breakpadinjector.dylib",
437    }.get(platform.system())
438
439    injector_lib = resolve_path((DIR.fetches,), "injector", injector_basename)
440    stackwalk = resolve_path((DIR.fetches,), "minidump_stackwalk", "minidump_stackwalk")
441    if stackwalk is not None:
442        env.setdefault("MINIDUMP_STACKWALK", stackwalk)
443    dump_syms = resolve_path((DIR.fetches,), "dump_syms", "dump_syms")
444    if dump_syms is not None:
445        env.setdefault("DUMP_SYMS", dump_syms)
446
447    if injector_lib is None:
448        use_minidump = False
449
450    info("use_minidump is {}".format(use_minidump))
451    info("  MINIDUMP_SAVE_PATH={}".format(env["MINIDUMP_SAVE_PATH"]))
452    info("  injector lib is {}".format(injector_lib))
453    info("  MINIDUMP_STACKWALK={}".format(env.get("MINIDUMP_STACKWALK")))
454
455
456mozconfig = os.path.join(DIR.source, "mozconfig.autospider")
457CONFIGURE_ARGS += " --prefix={OBJDIR}/dist".format(OBJDIR=quote(OBJDIR))
458
459# Generate a mozconfig.
460with open(mozconfig, "wt") as fh:
461    if AUTOMATION and platform.system() == "Windows":
462        fh.write('. "$topsrcdir/build/%s/mozconfig.vs-latest"\n' % variant_platform)
463    fh.write("ac_add_options --enable-project=js\n")
464    fh.write("ac_add_options " + CONFIGURE_ARGS + "\n")
465    fh.write("mk_add_options MOZ_OBJDIR=" + quote(OBJDIR) + "\n")
466
467env["MOZCONFIG"] = mozconfig
468
469mach = posixpath.join(PDIR.source, "mach")
470
471if not args.nobuild:
472    # Do the build
473    run_command([sys.executable, mach, "build"], check=True)
474
475    if use_minidump:
476        # Convert symbols to breakpad format.
477        cmd_env = env.copy()
478        cmd_env["MOZ_SOURCE_REPO"] = "file://" + DIR.source
479        cmd_env["RUSTC_COMMIT"] = "0"
480        cmd_env["MOZ_CRASHREPORTER"] = "1"
481        cmd_env["MOZ_AUTOMATION_BUILD_SYMBOLS"] = "1"
482        run_command(
483            [
484                sys.executable,
485                mach,
486                "build",
487                "recurse_syms",
488            ],
489            check=True,
490            env=cmd_env,
491        )
492
493COMMAND_PREFIX = []
494# On Linux, disable ASLR to make shell builds a bit more reproducible.
495if subprocess.call("type setarch >/dev/null 2>&1", shell=True) == 0:
496    COMMAND_PREFIX.extend(["setarch", platform.machine(), "-R"])
497
498
499def run_test_command(command, **kwargs):
500    _, _, status = run_command(COMMAND_PREFIX + command, check=False, **kwargs)
501    return status
502
503
504default_test_suites = frozenset(["jstests", "jittest", "jsapitests", "checks"])
505nondefault_test_suites = frozenset(["gdb"])
506all_test_suites = default_test_suites | nondefault_test_suites
507
508test_suites = set(default_test_suites)
509
510
511def normalize_tests(tests):
512    if "all" in tests:
513        return default_test_suites
514    return tests
515
516
517# Override environment variant settings conditionally.
518for k, v in variant.get("conditional-env", {}).get(variant_platform, {}).items():
519    env[k] = v.format(**REPLACEMENTS)
520
521# Skip any tests that are not run on this platform (or the 'all' platform).
522test_suites -= set(
523    normalize_tests(variant.get("skip-tests", {}).get(variant_platform, []))
524)
525test_suites -= set(normalize_tests(variant.get("skip-tests", {}).get("all", [])))
526
527# Add in additional tests for this platform (or the 'all' platform).
528test_suites |= set(
529    normalize_tests(variant.get("extra-tests", {}).get(variant_platform, []))
530)
531test_suites |= set(normalize_tests(variant.get("extra-tests", {}).get("all", [])))
532
533# Now adjust the variant's default test list with command-line arguments.
534test_suites |= set(normalize_tests(args.run_tests.split(",")))
535test_suites -= set(normalize_tests(args.skip_tests.split(",")))
536if "all" in args.skip_tests.split(","):
537    test_suites = []
538
539# Bug 1391877 - Windows test runs are getting mysterious timeouts when run
540# through taskcluster, but only when running multiple jit-test jobs in
541# parallel. Work around them for now.
542if platform.system() == "Windows":
543    env["JITTEST_EXTRA_ARGS"] = "-j1 " + env.get("JITTEST_EXTRA_ARGS", "")
544
545# Bug 1557130 - Atomics tests can create many additional threads which can
546# lead to resource exhaustion, resulting in intermittent failures. This was
547# only seen on beefy machines (> 32 cores), so limit the number of parallel
548# workers for now.
549if platform.system() == "Windows":
550    worker_count = min(multiprocessing.cpu_count(), 16)
551    env["JSTESTS_EXTRA_ARGS"] = "-j{} ".format(worker_count) + env.get(
552        "JSTESTS_EXTRA_ARGS", ""
553    )
554
555if use_minidump:
556    # Set up later js invocations to run with the breakpad injector loaded.
557    # Originally, I intended for this to be used with LD_PRELOAD, but when
558    # cross-compiling from 64- to 32-bit, that will fail and produce stderr
559    # output when running any 64-bit commands, which breaks eg mozconfig
560    # processing. So use the --dll command line mechanism universally.
561    for v in ("JSTESTS_EXTRA_ARGS", "JITTEST_EXTRA_ARGS"):
562        env[v] = "--args='--dll %s' %s" % (injector_lib, env.get(v, ""))
563
564# Always run all enabled tests, even if earlier ones failed. But return the
565# first failed status.
566results = [("(make-nonempty)", 0)]
567
568if "checks" in test_suites:
569    results.append(("make check", run_test_command([MAKE, "check"])))
570
571if "jittest" in test_suites:
572    results.append(("make check-jit-test", run_test_command([MAKE, "check-jit-test"])))
573if "jsapitests" in test_suites:
574    jsapi_test_binary = os.path.join(OBJDIR, "dist", "bin", "jsapi-tests")
575    test_env = env.copy()
576    test_env["TOPSRCDIR"] = DIR.source
577    if use_minidump and platform.system() == "Linux":
578        test_env["LD_PRELOAD"] = injector_lib
579    st = run_test_command([jsapi_test_binary], env=test_env)
580    if st < 0:
581        print("PROCESS-CRASH | jsapi-tests | application crashed")
582        print("Return code: {}".format(st))
583    results.append(("jsapi-tests", st))
584if "jstests" in test_suites:
585    results.append(("jstests", run_test_command([MAKE, "check-jstests"])))
586if "gdb" in test_suites:
587    test_script = os.path.join(DIR.js_src, "gdb", "run-tests.py")
588    auto_args = ["-s", "-o", "--no-progress"] if AUTOMATION else []
589    extra_args = env.get("GDBTEST_EXTRA_ARGS", "").split(" ")
590    results.append(
591        (
592            "gdb",
593            run_test_command([PYTHON, test_script, *auto_args, *extra_args, OBJDIR]),
594        )
595    )
596
597# FIXME bug 1291449: This would be unnecessary if we could run msan with -mllvm
598# -msan-keep-going, but in clang 3.8 it causes a hang during compilation.
599if variant.get("ignore-test-failures"):
600    logging.warning("Ignoring test results %s" % (results,))
601    results = [("ignored", 0)]
602
603if args.variant == "msan":
604    files = filter(lambda f: f.startswith("sanitize_log."), os.listdir(OUTDIR))
605    fullfiles = [os.path.join(OUTDIR, f) for f in files]
606
607    # Summarize results
608    sites = Counter()
609    errors = Counter()
610    for filename in fullfiles:
611        with open(os.path.join(OUTDIR, filename), "rb") as fh:
612            for line in fh:
613                m = re.match(
614                    r"^SUMMARY: \w+Sanitizer: (?:data race|use-of-uninitialized-value) (.*)",  # NOQA: E501
615                    line.strip(),
616                )
617                if m:
618                    # Some reports include file:line:column, some just
619                    # file:line. Just in case it's nondeterministic, we will
620                    # canonicalize to just the line number.
621                    site = re.sub(r"^(\S+?:\d+)(:\d+)* ", r"\1 ", m.group(1))
622                    sites[site] += 1
623
624    # Write a summary file and display it to stdout.
625    summary_filename = os.path.join(
626        env["MOZ_UPLOAD_DIR"], "%s_summary.txt" % args.variant
627    )
628    with open(summary_filename, "wb") as outfh:
629        for location, count in sites.most_common():
630            print >> outfh, "%d %s" % (count, location)
631    print(open(summary_filename, "rb").read())
632
633    if "max-errors" in variant:
634        max_allowed = variant["max-errors"]
635        print("Found %d errors out of %d allowed" % (len(sites), max_allowed))
636        if len(sites) > max_allowed:
637            results.append(("too many msan errors", 1))
638
639    # Gather individual results into a tarball. Note that these are
640    # distinguished only by pid of the JS process running within each test, so
641    # given the 16-bit limitation of pids, it's totally possible that some of
642    # these files will be lost due to being overwritten.
643    command = [
644        "tar",
645        "-C",
646        OUTDIR,
647        "-zcf",
648        os.path.join(env["MOZ_UPLOAD_DIR"], "%s.tar.gz" % args.variant),
649    ]
650    command += files
651    subprocess.call(command)
652
653# Generate stacks from minidumps.
654if use_minidump:
655    venv_python = os.path.join(OBJDIR, "_virtualenvs", "build", "bin", "python3")
656    run_command(
657        [
658            venv_python,
659            os.path.join(DIR.source, "testing/mozbase/mozcrash/mozcrash/mozcrash.py"),
660            os.getenv("TMPDIR", "/tmp"),
661            os.path.join(OBJDIR, "dist/crashreporter-symbols"),
662        ]
663    )
664
665for name, st in results:
666    print("exit status %d for '%s'" % (st, name))
667
668# Pick the "worst" exit status. SIGSEGV might give a status of -11, so use the
669# maximum absolute value instead of just the maximum.
670exit_status = max((st for _, st in results), key=abs)
671
672# The exit status on Windows can be something like 2147483651 (0x80000003),
673# which will be converted to status zero in the caller. Mask off the high bits,
674# but if the result is zero then fall back to returning 1.
675if exit_status & 0xFF:
676    sys.exit(exit_status & 0xFF)
677else:
678    sys.exit(1 if exit_status else 0)
679