tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

building.py (66583B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import errno
      6 import getpass
      7 import io
      8 import json
      9 import logging
     10 import os
     11 import re
     12 import subprocess
     13 import sys
     14 import time
     15 from collections import Counter, OrderedDict, namedtuple
     16 from itertools import dropwhile, islice, takewhile
     17 from textwrap import TextWrapper
     18 
     19 from mach.site import CommandSiteManager
     20 
     21 try:
     22    import psutil
     23 except Exception:
     24    psutil = None
     25 
     26 import mozfile
     27 import mozpack.path as mozpath
     28 from mach.mixin.logging import LoggingMixin
     29 from mach.util import get_state_dir, get_virtualenv_base_dir
     30 from mozsystemmonitor.resourcemonitor import SystemResourceMonitor
     31 from mozterm.widgets import Footer
     32 
     33 from ..backend import get_backend_class
     34 from ..base import MozbuildObject
     35 from ..compilation.warnings import WarningsCollector, WarningsDatabase
     36 from ..dirutils import mkdir
     37 from ..serialized_logging import read_serialized_record
     38 from ..telemetry import get_cpu_brand
     39 from ..testing import install_test_files
     40 from ..util import FileAvoidWrite, resolve_target_to_make
     41 from .clobber import Clobberer
     42 
     43 FINDER_SLOW_MESSAGE = """
     44 ===================
     45 PERFORMANCE WARNING
     46 
     47 The OS X Finder application (file indexing used by Spotlight) used a lot of CPU
     48 during the build - an average of %f%% (100%% is 1 core). This made your build
     49 slower.
     50 
     51 Consider adding ".noindex" to the end of your object directory name to have
     52 Finder ignore it. Or, add an indexing exclusion through the Spotlight System
     53 Preferences.
     54 ===================
     55 """.strip()
     56 
     57 
     58 INSTALL_TESTS_CLOBBER = "".join([
     59    TextWrapper().fill(line) + "\n"
     60    for line in """
     61 The build system was unable to install tests because the CLOBBER file has \
     62 been updated. This means if you edited any test files, your changes may not \
     63 be picked up until a full/clobber build is performed.
     64 
     65 The easiest and fastest way to perform a clobber build is to run:
     66 
     67 $ mach clobber
     68 $ mach build
     69 
     70 If you did not modify any test files, it is safe to ignore this message \
     71 and proceed with running tests. To do this run:
     72 
     73 $ touch {clobber_file}
     74 """.splitlines()
     75 ])
     76 
     77 CLOBBER_REQUESTED_MESSAGE = """
     78 ===================
     79 The CLOBBER file was updated prior to this build. A clobber build may be
     80 required to succeed, but we weren't expecting it to.
     81 
     82 Please consider filing a bug for this failure if you have reason to believe
     83 this is a clobber bug and not due to local changes.
     84 ===================
     85 """.strip()
     86 
     87 
     88 BuildOutputResult = namedtuple(
     89    "BuildOutputResult", ("warning", "state_changed", "message")
     90 )
     91 
     92 
     93 class TierStatus:
     94    """Represents the state and progress of tier traversal.
     95 
     96    The build system is organized into linear phases called tiers. Each tier
     97    executes in the order it was defined, 1 at a time.
     98    """
     99 
    100    def __init__(self, resources, metrics):
    101        """Accepts a SystemResourceMonitor to record results against."""
    102        self.tiers = OrderedDict()
    103        self.tier_status = OrderedDict()
    104        self.resources = resources
    105        self.metrics = metrics
    106 
    107    def set_tiers(self, tiers):
    108        """Record the set of known tiers."""
    109        for tier in tiers:
    110            self.tiers[tier] = dict(
    111                begin_time=None,
    112                finish_time=None,
    113                duration=None,
    114            )
    115            self.tier_status[tier] = None
    116 
    117    def begin_tier(self, tier):
    118        """Record that execution of a tier has begun."""
    119        self.tier_status[tier] = "active"
    120        t = self.tiers[tier]
    121        t["begin_time"] = time.monotonic()
    122        self.resources.begin_phase(tier)
    123        metrics_tier_name = "tier_" + tier.replace("-", "_") + "_duration"
    124        metrics_attribute = getattr(self.metrics.mozbuild, metrics_tier_name, None)
    125        if metrics_attribute:
    126            metrics_attribute.start()
    127 
    128    def finish_tier(self, tier):
    129        """Record that execution of a tier has finished."""
    130        self.tier_status[tier] = "finished"
    131        t = self.tiers[tier]
    132        t["finish_time"] = time.monotonic()
    133        t["duration"] = self.resources.finish_phase(tier)
    134        metrics_tier_name = "tier_" + tier.replace("-", "_") + "_duration"
    135        metrics_attribute = getattr(self.metrics.mozbuild, metrics_tier_name, None)
    136        if metrics_attribute:
    137            metrics_attribute.stop()
    138 
    139 
    140 def record_cargo_timings(resource_monitor, timings_path):
    141    cargo_start = 0
    142    try:
    143        with open(timings_path) as fh:
    144            # Extrace the UNIT_DATA list from the cargo timing HTML file.
    145            unit_data = dropwhile(lambda l: l.rstrip() != "const UNIT_DATA = [", fh)
    146            unit_data = islice(unit_data, 1, None)
    147            lines = takewhile(lambda l: l.rstrip() != "];", unit_data)
    148            entries = json.loads("[" + "".join(lines) + "]")
    149            # Normalize the entries so that any change in data format would
    150            # trigger the exception handler that skips this (we don't want the
    151            # build to fail in that case)
    152            data = [
    153                (
    154                    "{} v{}{}".format(
    155                        entry["name"], entry["version"], entry.get("target", "")
    156                    ),
    157                    entry["start"] or 0,
    158                    entry["duration"] or 0,
    159                )
    160                for entry in entries
    161            ]
    162        starts = [
    163            start
    164            for marker, start in resource_monitor._active_markers.items()
    165            if marker.startswith("Rust:")
    166        ]
    167        # The build system is not supposed to be running more than one cargo
    168        # at the same time, which thankfully makes it easier to find the start
    169        # of the one we got the timings for.
    170        if len(starts) != 1:
    171            return
    172        cargo_start = starts[0]
    173    except Exception:
    174        return
    175 
    176    if not cargo_start:
    177        return
    178 
    179    for name, start, duration in data:
    180        resource_monitor.record_marker(
    181            "RustCrate", cargo_start + start, cargo_start + start + duration, name
    182        )
    183 
    184 
    185 class BuildMonitor(MozbuildObject):
    186    """Monitors the output of the build."""
    187 
    188    def init(self, warnings_path, terminal, metrics):
    189        """Create a new monitor.
    190 
    191        warnings_path is a path of a warnings database to use.
    192        """
    193        self._warnings_path = warnings_path
    194        self.resources = SystemResourceMonitor(
    195            poll_interval=0.1,
    196            metadata={"CPUName": get_cpu_brand()},
    197        )
    198        self._resources_started = False
    199 
    200        self.tiers = TierStatus(self.resources, metrics)
    201 
    202        self.warnings_database = WarningsDatabase()
    203        if os.path.exists(warnings_path):
    204            try:
    205                self.warnings_database.load_from_file(warnings_path)
    206            except ValueError:
    207                os.remove(warnings_path)
    208 
    209        # Contains warnings unique to this invocation. Not populated with old
    210        # warnings.
    211        self.instance_warnings = WarningsDatabase()
    212 
    213        self._terminal = terminal
    214 
    215        def on_warning(warning):
    216            # Skip `errors`
    217            if warning["type"] == "error":
    218                return
    219 
    220            filename = warning["filename"]
    221 
    222            if not os.path.exists(filename):
    223                raise Exception("Could not find file containing warning: %s" % filename)
    224 
    225            self.warnings_database.insert(warning)
    226            # Make a copy so mutations don't impact other database.
    227            self.instance_warnings.insert(warning.copy())
    228 
    229        self._warnings_collector = WarningsCollector(on_warning, objdir=self.topobjdir)
    230 
    231        self.build_objects = []
    232        self.build_dirs = set()
    233 
    234    def start(self):
    235        """Record the start of the build."""
    236        self.start_time = time.monotonic()
    237        self._finder_start_cpu = self._get_finder_cpu_usage()
    238 
    239    def start_resource_recording(self):
    240        # This should be merged into start() once bug 892342 lands.
    241        self.resources.start()
    242        self._resources_started = True
    243 
    244    def on_line(self, line):
    245        """Consume a line of output from the build system.
    246 
    247        This will parse the line for state and determine whether more action is
    248        needed.
    249 
    250        Returns a BuildOutputResult instance.
    251 
    252        In this named tuple, warning will be an object describing a new parsed
    253        warning. Otherwise it will be None.
    254 
    255        state_changed indicates whether the build system changed state with
    256        this line. If the build system changed state, the caller may want to
    257        query this instance for the current state in order to update UI, etc.
    258 
    259        message is either None, or the content of a message to be
    260        displayed to the user (as a str or a logging.LogRecord).
    261        """
    262        message = None
    263 
    264        # If the previous line was colored (eg. for a compiler warning), our
    265        # line will start with the ansi reset sequence. Strip it to ensure it
    266        # does not interfere with our parsing of the line.
    267        plain_line = self._terminal.strip(line) if self._terminal else line.strip()
    268        if plain_line.startswith("BUILDSTATUS"):
    269            args = plain_line.split()
    270 
    271            _, _, disambiguator = args.pop(0).partition("@")
    272            action = args.pop(0)
    273            time = None
    274            regexp = re.compile(r"\d{10}(\.\d{1,9})?$")
    275            if regexp.match(action):
    276                time = float(action)
    277                action = args.pop(0)
    278 
    279            update_needed = True
    280 
    281            if action == "TIERS":
    282                self.tiers.set_tiers(args)
    283                update_needed = False
    284            elif action == "TIER_START":
    285                tier = args[0]
    286                self.tiers.begin_tier(tier)
    287            elif action == "TIER_FINISH":
    288                (tier,) = args
    289                self.tiers.finish_tier(tier)
    290            elif action == "OBJECT_FILE":
    291                self.build_objects.append(args[0])
    292                self.resources.begin_marker("Object", args[0], disambiguator)
    293                update_needed = False
    294            elif action.startswith("START_"):
    295                self.resources.begin_marker(
    296                    action[len("START_") :], " ".join(args), disambiguator, time
    297                )
    298                update_needed = False
    299            elif action.startswith("END_"):
    300                self.resources.end_marker(
    301                    action[len("END_") :], " ".join(args), disambiguator, time
    302                )
    303                update_needed = False
    304            elif action == "BUILD_VERBOSE":
    305                build_dir = args[0]
    306                if build_dir not in self.build_dirs:
    307                    self.build_dirs.add(build_dir)
    308                    message = build_dir
    309                update_needed = False
    310            else:
    311                raise Exception("Unknown build status: %s" % action)
    312 
    313            return BuildOutputResult(None, update_needed, message)
    314 
    315        elif plain_line.startswith("Timing report saved to "):
    316            cargo_timings = plain_line[len("Timing report saved to ") :]
    317            record_cargo_timings(self.resources, cargo_timings)
    318            return BuildOutputResult(None, False, None)
    319 
    320        if log_record := read_serialized_record(line):
    321            return BuildOutputResult(None, False, log_record)
    322 
    323        warning = None
    324        message = line
    325 
    326        try:
    327            warning = self._warnings_collector.process_line(line)
    328        except Exception:
    329            pass
    330 
    331        return BuildOutputResult(warning, False, message)
    332 
    333    def stop_resource_recording(self):
    334        if self._resources_started:
    335            self.resources.stop()
    336 
    337        self._resources_started = False
    338 
    339    def finish(self):
    340        """Record the end of the build."""
    341        self.stop_resource_recording()
    342        self.end_time = time.monotonic()
    343        self._finder_end_cpu = self._get_finder_cpu_usage()
    344        self.elapsed = self.end_time - self.start_time
    345 
    346        self.warnings_database.prune()
    347        self.warnings_database.save_to_file(self._warnings_path)
    348 
    349    def record_usage(self):
    350        build_resources_profile_path = None
    351        try:
    352            # When running on automation, we store the resource usage data in
    353            # the upload path, alongside, for convenience, a copy of the HTML
    354            # viewer.
    355            if "MOZ_AUTOMATION" in os.environ and "UPLOAD_PATH" in os.environ:
    356                build_resources_profile_path = mozpath.join(
    357                    os.environ["UPLOAD_PATH"], "profile_build_resources.json"
    358                )
    359            else:
    360                build_resources_profile_path = self._get_state_filename(
    361                    "profile_build_resources.json"
    362                )
    363            with open(
    364                build_resources_profile_path, "w", encoding="utf-8", newline="\n"
    365            ) as fh:
    366                to_write = json.dumps(
    367                    self.resources.as_profile(), separators=(",", ":")
    368                )
    369                fh.write(to_write)
    370        except Exception as e:
    371            self.log(
    372                logging.WARNING,
    373                "build_resources_error",
    374                {"msg": str(e)},
    375                "Exception when writing resource usage file: {msg}",
    376            )
    377            try:
    378                if build_resources_profile_path and os.path.exists(
    379                    build_resources_profile_path
    380                ):
    381                    os.remove(build_resources_profile_path)
    382            except Exception:
    383                # In case there's an exception for some reason, ignore it.
    384                pass
    385 
    386    def _get_finder_cpu_usage(self):
    387        """Obtain the CPU usage of the Finder app on OS X.
    388 
    389        This is used to detect high CPU usage.
    390        """
    391        if not sys.platform.startswith("darwin"):
    392            return None
    393 
    394        if not psutil:
    395            return None
    396 
    397        for proc in psutil.process_iter():
    398            if proc.name != "Finder":
    399                continue
    400 
    401            if proc.username != getpass.getuser():
    402                continue
    403 
    404            # Try to isolate system finder as opposed to other "Finder"
    405            # processes.
    406            if not proc.exe.endswith("CoreServices/Finder.app/Contents/MacOS/Finder"):
    407                continue
    408 
    409            return proc.get_cpu_times()
    410 
    411        return None
    412 
    413    def have_high_finder_usage(self):
    414        """Determine whether there was high Finder CPU usage during the build.
    415 
    416        Returns True if there was high Finder CPU usage, False if there wasn't,
    417        or None if there is nothing to report.
    418        """
    419        if not self._finder_start_cpu:
    420            return None, None
    421 
    422        # We only measure if the measured range is sufficiently long.
    423        if self.elapsed < 15:
    424            return None, None
    425 
    426        if not self._finder_end_cpu:
    427            return None, None
    428 
    429        start = self._finder_start_cpu
    430        end = self._finder_end_cpu
    431 
    432        start_total = start.user + start.system
    433        end_total = end.user + end.system
    434 
    435        cpu_seconds = end_total - start_total
    436 
    437        # If Finder used more than 25% of 1 core during the build, report an
    438        # error.
    439        finder_percent = cpu_seconds / self.elapsed * 100
    440 
    441        return finder_percent > 25, finder_percent
    442 
    443    def have_excessive_swapping(self):
    444        """Determine whether there was excessive swapping during the build.
    445 
    446        Returns a tuple of (excessive, swap_in, swap_out). All values are None
    447        if no swap information is available.
    448        """
    449        if not self.have_resource_usage:
    450            return None, None, None
    451 
    452        swap_in = sum(m.swap.sin for m in self.resources.measurements)
    453        swap_out = sum(m.swap.sout for m in self.resources.measurements)
    454 
    455        # The threshold of 1024 MB has been arbitrarily chosen.
    456        #
    457        # Choosing a proper value that is ideal for everyone is hard. We will
    458        # likely iterate on the logic until people are generally satisfied.
    459        # If a value is too low, the eventual warning produced does not carry
    460        # much meaning. If the threshold is too high, people may not see the
    461        # warning and the warning will thus be ineffective.
    462        excessive = swap_in > 512 * 1048576 or swap_out > 512 * 1048576
    463        return excessive, swap_in, swap_out
    464 
    465    @property
    466    def have_resource_usage(self):
    467        """Whether resource usage is available."""
    468        return self.resources.start_time is not None
    469 
    470    def get_resource_usage(self):
    471        """Produce a data structure containing the low-level resource usage information.
    472 
    473        This data structure can e.g. be serialized into JSON and saved for
    474        subsequent analysis.
    475 
    476        If no resource usage is available, None is returned.
    477        """
    478        if not self.have_resource_usage:
    479            return None
    480 
    481        cpu_percent = self.resources.aggregate_cpu_percent(phase=None, per_cpu=False)
    482        io = self.resources.aggregate_io(phase=None)
    483 
    484        return dict(
    485            cpu_percent=cpu_percent,
    486            io=io,
    487        )
    488 
    489    def log_resource_usage(self, usage):
    490        """Summarize the resource usage of this build in a log message."""
    491 
    492        if not usage:
    493            return
    494 
    495        params = dict(
    496            duration=self.end_time - self.start_time,
    497            cpu_percent=usage["cpu_percent"],
    498            io_read_bytes=usage["io"].read_bytes,
    499            io_write_bytes=usage["io"].write_bytes,
    500        )
    501 
    502        message = (
    503            "Overall system resources - Wall time: {duration:.0f}s; "
    504            "CPU: {cpu_percent:.0f}%; "
    505            "Read bytes: {io_read_bytes}; Write bytes: {io_write_bytes}; "
    506        )
    507 
    508        if hasattr(usage["io"], "read_time") and hasattr(usage["io"], "write_time"):
    509            params.update(
    510                io_read_time=usage["io"].read_time,
    511                io_write_time=usage["io"].write_time,
    512            )
    513            message += "Read time: {io_read_time}; Write time: {io_write_time}"
    514 
    515        self.log(logging.WARNING, "resource_usage", params, message)
    516 
    517        excessive, sin, sout = self.have_excessive_swapping()
    518        if excessive is not None and (sin or sout):
    519            sin /= 1048576
    520            sout /= 1048576
    521            self.log(
    522                logging.WARNING,
    523                "swap_activity",
    524                {"sin": sin, "sout": sout},
    525                "Swap in/out (MB): {sin}/{sout}",
    526            )
    527 
    528    def ccache_stats(self, ccache=None):
    529        ccache_stats = None
    530 
    531        if ccache is None:
    532            ccache = mozfile.which("ccache")
    533        if ccache:
    534            # With CCache v3.7+ we can use --print-stats
    535            has_machine_format = CCacheStats.check_version_3_7_or_newer(ccache)
    536            try:
    537                output = subprocess.check_output(
    538                    [ccache, "--print-stats" if has_machine_format else "-s"],
    539                    universal_newlines=True,
    540                )
    541                ccache_stats = CCacheStats(output, has_machine_format)
    542            except ValueError as e:
    543                self.log(logging.WARNING, "ccache", {"msg": str(e)}, "{msg}")
    544        return ccache_stats
    545 
    546 
    547 class TerminalLoggingHandler(logging.Handler):
    548    """Custom logging handler that works with terminal window dressing.
    549 
    550    This class should probably live elsewhere, like the mach core. Consider
    551    this a proving ground for its usefulness.
    552    """
    553 
    554    def __init__(self):
    555        logging.Handler.__init__(self)
    556 
    557        self.fh = sys.stdout
    558        self.footer = None
    559 
    560    def flush(self):
    561        self.acquire()
    562 
    563        try:
    564            self.fh.flush()
    565        finally:
    566            self.release()
    567 
    568    def emit(self, record):
    569        msg = self.format(record)
    570 
    571        self.acquire()
    572 
    573        try:
    574            if self.footer:
    575                self.footer.clear()
    576 
    577            self.fh.write(msg)
    578            self.fh.write("\n")
    579 
    580            if self.footer:
    581                self.footer.draw()
    582 
    583            # If we don't flush, the footer may not get drawn.
    584            self.fh.flush()
    585        finally:
    586            self.release()
    587 
    588 
    589 class BuildProgressFooter(Footer):
    590    """Handles display of a build progress indicator in a terminal.
    591 
    592    When mach builds inside a blessed-supported terminal, it will render
    593    progress information collected from a BuildMonitor. This class converts the
    594    state of BuildMonitor into terminal output.
    595    """
    596 
    597    def __init__(self, terminal, monitor):
    598        Footer.__init__(self, terminal)
    599        self.tiers = monitor.tiers.tier_status.items()
    600 
    601    def draw(self):
    602        """Draws this footer in the terminal."""
    603 
    604        if not self.tiers:
    605            return
    606 
    607        # The drawn terminal looks something like:
    608        # TIER: static export libs tools
    609 
    610        parts = [("bold", "TIER:")]
    611        append = parts.append
    612        for tier, status in self.tiers:
    613            if status is None:
    614                append(tier)
    615            elif status == "finished":
    616                append(("green", tier))
    617            else:
    618                append(("underline_yellow", tier))
    619 
    620        self.write(parts)
    621 
    622 
    623 class OutputManager(LoggingMixin):
    624    """Handles writing job output to a terminal or log."""
    625 
    626    def __init__(self, log_manager, footer):
    627        self.populate_logger()
    628 
    629        self.footer = None
    630        terminal = log_manager.terminal
    631 
    632        # TODO convert terminal footer to config file setting.
    633        if not terminal:
    634            return
    635        if os.environ.get("INSIDE_EMACS", None):
    636            return
    637 
    638        if os.environ.get("MACH_NO_TERMINAL_FOOTER", None):
    639            footer = None
    640 
    641        self.t = terminal
    642        self.footer = footer
    643 
    644        self._handler = TerminalLoggingHandler()
    645        self._handler.setFormatter(log_manager.terminal_formatter)
    646        self._handler.footer = self.footer
    647 
    648        old = log_manager.replace_terminal_handler(self._handler)
    649        self._handler.level = old.level
    650 
    651    def __enter__(self):
    652        return self
    653 
    654    def __exit__(self, exc_type, exc_value, traceback):
    655        if self.footer:
    656            self.footer.clear()
    657            # Prevents the footer from being redrawn if logging occurs.
    658            self._handler.footer = None
    659 
    660    def write_line(self, line):
    661        if self.footer:
    662            self.footer.clear()
    663 
    664        print(line)
    665 
    666        if self.footer:
    667            self.footer.draw()
    668 
    669    def refresh(self):
    670        if not self.footer:
    671            return
    672 
    673        self.footer.clear()
    674        self.footer.draw()
    675 
    676 
    677 class BuildOutputManager(OutputManager):
    678    """Handles writing build output to a terminal, to logs, etc."""
    679 
    680    def __init__(self, log_manager, monitor, footer):
    681        self.monitor = monitor
    682        OutputManager.__init__(self, log_manager, footer)
    683 
    684    def __exit__(self, exc_type, exc_value, traceback):
    685        OutputManager.__exit__(self, exc_type, exc_value, traceback)
    686 
    687        # Ensure the resource monitor is stopped because leaving it running
    688        # could result in the process hanging on exit because the resource
    689        # collection child process hasn't been told to stop.
    690        self.monitor.stop_resource_recording()
    691 
    692    def on_line(self, line):
    693        warning, state_changed, message = self.monitor.on_line(line)
    694 
    695        if message:
    696            if isinstance(message, logging.LogRecord):
    697                self.log_record("build_output", message)
    698            else:
    699                self.log(logging.INFO, "build_output", {"line": message}, "{line}")
    700        elif state_changed:
    701            have_handler = hasattr(self, "_handler")
    702            if have_handler:
    703                self._handler.acquire()
    704            try:
    705                self.refresh()
    706            finally:
    707                if have_handler:
    708                    self._handler.release()
    709 
    710 
    711 class StaticAnalysisFooter(Footer):
    712    """Handles display of a static analysis progress indicator in a terminal."""
    713 
    714    def __init__(self, terminal, monitor):
    715        Footer.__init__(self, terminal)
    716        self.monitor = monitor
    717 
    718    def draw(self):
    719        """Draws this footer in the terminal."""
    720 
    721        monitor = self.monitor
    722        total = monitor.num_files
    723        processed = monitor.num_files_processed
    724        percent = "(%.2f%%)" % (processed * 100.0 / total)
    725        parts = [
    726            ("bright_black", "Processing"),
    727            ("yellow", str(processed)),
    728            ("bright_black", "of"),
    729            ("yellow", str(total)),
    730            ("bright_black", "files"),
    731            ("green", percent),
    732        ]
    733        if monitor.current_file:
    734            parts.append(("bold", monitor.current_file))
    735 
    736        self.write(parts)
    737 
    738 
    739 class StaticAnalysisOutputManager(OutputManager):
    740    """Handles writing static analysis output to a terminal or file."""
    741 
    742    def __init__(self, log_manager, monitor, footer):
    743        self.monitor = monitor
    744        self.raw = ""
    745        OutputManager.__init__(self, log_manager, footer)
    746 
    747    def on_line(self, line):
    748        warning, relevant = self.monitor.on_line(line)
    749        if relevant:
    750            self.raw += line + "\n"
    751 
    752        if warning:
    753            self.log(
    754                logging.INFO,
    755                "compiler_warning",
    756                warning,
    757                "Warning: {flag} in {filename}: {message}",
    758            )
    759 
    760        if relevant:
    761            self.log(logging.INFO, "build_output", {"line": line}, "{line}")
    762        else:
    763            have_handler = hasattr(self, "_handler")
    764            if have_handler:
    765                self._handler.acquire()
    766            try:
    767                self.refresh()
    768            finally:
    769                if have_handler:
    770                    self._handler.release()
    771 
    772    def write(self, path, output_format):
    773        assert output_format in (
    774            "text",
    775            "json",
    776        ), f"Invalid output format {output_format}"
    777        path = mozpath.realpath(path)
    778 
    779        if output_format == "json":
    780            self.monitor._warnings_database.save_to_file(path)
    781 
    782        else:
    783            with open(path, "w", encoding="utf-8", newline="\n") as f:
    784                f.write(self.raw)
    785 
    786        self.log(
    787            logging.INFO,
    788            "write_output",
    789            {"path": path, "format": output_format},
    790            "Wrote {format} output in {path}",
    791        )
    792 
    793 
    794 class CCacheStats:
    795    """Holds statistics from ccache.
    796 
    797    Instances can be subtracted from each other to obtain differences.
    798    print() or str() the object to show a ``ccache -s`` like output
    799    of the captured stats.
    800 
    801    """
    802 
    803    STATS_KEYS = [
    804        # (key, description)
    805        # Refer to stats.c in ccache project for all the descriptions.
    806        ("stats_zeroed", ("stats zeroed", "stats zero time")),
    807        ("stats_updated", "stats updated"),
    808        ("cache_hit_direct", "cache hit (direct)"),
    809        ("cache_hit_preprocessed", "cache hit (preprocessed)"),
    810        ("cache_hit_rate", "cache hit rate"),
    811        ("cache_miss", "cache miss"),
    812        ("link", "called for link"),
    813        ("preprocessing", "called for preprocessing"),
    814        ("multiple", "multiple source files"),
    815        ("stdout", "compiler produced stdout"),
    816        ("no_output", "compiler produced no output"),
    817        ("empty_output", "compiler produced empty output"),
    818        ("failed", "compile failed"),
    819        ("error", "ccache internal error"),
    820        ("preprocessor_error", "preprocessor error"),
    821        ("cant_use_pch", "can't use precompiled header"),
    822        ("compiler_missing", "couldn't find the compiler"),
    823        ("cache_file_missing", "cache file missing"),
    824        ("bad_args", "bad compiler arguments"),
    825        ("unsupported_lang", "unsupported source language"),
    826        ("compiler_check_failed", "compiler check failed"),
    827        ("autoconf", "autoconf compile/link"),
    828        ("unsupported_code_directive", "unsupported code directive"),
    829        ("unsupported_compiler_option", "unsupported compiler option"),
    830        ("out_stdout", "output to stdout"),
    831        ("out_device", "output to a non-regular file"),
    832        ("no_input", "no input file"),
    833        ("bad_extra_file", "error hashing extra file"),
    834        ("num_cleanups", "cleanups performed"),
    835        ("cache_files", "files in cache"),
    836        ("cache_size", "cache size"),
    837        ("cache_max_size", "max cache size"),
    838    ]
    839 
    840    SKIP_LINES = (
    841        "cache directory",
    842        "primary config",
    843        "secondary config",
    844    )
    845 
    846    STATS_KEYS_3_7_PLUS = {
    847        "stats_zeroed_timestamp": "stats_zeroed",
    848        "stats_updated_timestamp": "stats_updated",
    849        "direct_cache_hit": "cache_hit_direct",
    850        "preprocessed_cache_hit": "cache_hit_preprocessed",
    851        # "cache_hit_rate" is not provided
    852        "cache_miss": "cache_miss",
    853        "called_for_link": "link",
    854        "called_for_preprocessing": "preprocessing",
    855        "multiple_source_files": "multiple",
    856        "compiler_produced_stdout": "stdout",
    857        "compiler_produced_no_output": "no_output",
    858        "compiler_produced_empty_output": "empty_output",
    859        "compile_failed": "failed",
    860        "internal_error": "error",
    861        "preprocessor_error": "preprocessor_error",
    862        "could_not_use_precompiled_header": "cant_use_pch",
    863        "could_not_find_compiler": "compiler_missing",
    864        "missing_cache_file": "cache_file_missing",
    865        "bad_compiler_arguments": "bad_args",
    866        "unsupported_source_language": "unsupported_lang",
    867        "compiler_check_failed": "compiler_check_failed",
    868        "autoconf_test": "autoconf",
    869        "unsupported_code_directive": "unsupported_code_directive",
    870        "unsupported_compiler_option": "unsupported_compiler_option",
    871        "output_to_stdout": "out_stdout",
    872        "output_to_a_non_file": "out_device",
    873        "no_input_file": "no_input",
    874        "error_hashing_extra_file": "bad_extra_file",
    875        "cleanups_performed": "num_cleanups",
    876        "files_in_cache": "cache_files",
    877        "cache_size_kibibyte": "cache_size",
    878        # "cache_max_size" is obsolete and not printed anymore
    879    }
    880 
    881    ABSOLUTE_KEYS = {"cache_files", "cache_size", "cache_max_size"}
    882    FORMAT_KEYS = {"cache_size", "cache_max_size"}
    883 
    884    GiB = 1024**3
    885    MiB = 1024**2
    886    KiB = 1024
    887 
    888    def __init__(self, output=None, has_machine_format=False):
    889        """Construct an instance from the output of ccache -s."""
    890        self._values = {}
    891 
    892        if not output:
    893            return
    894 
    895        if has_machine_format:
    896            self._parse_machine_format(output)
    897        else:
    898            self._parse_human_format(output)
    899 
    900    def _parse_machine_format(self, output):
    901        for line in output.splitlines():
    902            line = line.strip()
    903            key, _, value = line.partition("\t")
    904            stat_key = self.STATS_KEYS_3_7_PLUS.get(key)
    905            if stat_key:
    906                value = int(value)
    907                if key.endswith("_kibibyte"):
    908                    value *= 1024
    909                self._values[stat_key] = value
    910 
    911        (direct, preprocessed, miss) = self.hit_rates()
    912        self._values["cache_hit_rate"] = (direct + preprocessed) * 100
    913 
    914    def _parse_human_format(self, output):
    915        for line in output.splitlines():
    916            line = line.strip()
    917            if line:
    918                self._parse_line(line)
    919 
    920    def _parse_line(self, line):
    921        for stat_key, stat_description in self.STATS_KEYS:
    922            if line.startswith(stat_description):
    923                raw_value = self._strip_prefix(line, stat_description)
    924                self._values[stat_key] = self._parse_value(raw_value)
    925                break
    926        else:
    927            if not line.startswith(self.SKIP_LINES):
    928                raise ValueError("Failed to parse ccache stats output: %s" % line)
    929 
    930    @staticmethod
    931    def _strip_prefix(line, prefix):
    932        if isinstance(prefix, tuple):
    933            for p in prefix:
    934                line = CCacheStats._strip_prefix(line, p)
    935            return line
    936        return line[len(prefix) :].strip() if line.startswith(prefix) else line
    937 
    938    @staticmethod
    939    def _parse_value(raw_value):
    940        try:
    941            # ccache calls strftime with '%c' (src/stats.c)
    942            ts = time.strptime(raw_value, "%c")
    943            return int(time.mktime(ts))
    944        except ValueError:
    945            if raw_value == "never":
    946                return 0
    947            pass
    948 
    949        value = raw_value.split()
    950        unit = ""
    951        if len(value) == 1:
    952            numeric = value[0]
    953        elif len(value) == 2:
    954            numeric, unit = value
    955        else:
    956            raise ValueError("Failed to parse ccache stats value: %s" % raw_value)
    957 
    958        if "." in numeric:
    959            numeric = float(numeric)
    960        else:
    961            numeric = int(numeric)
    962 
    963        if unit in ("GB", "Gbytes"):
    964            unit = CCacheStats.GiB
    965        elif unit in ("MB", "Mbytes"):
    966            unit = CCacheStats.MiB
    967        elif unit in ("KB", "Kbytes"):
    968            unit = CCacheStats.KiB
    969        else:
    970            unit = 1
    971 
    972        return int(numeric * unit)
    973 
    974    def hit_rate_message(self):
    975        return (
    976            "ccache (direct) hit rate: {:.1%}; (preprocessed) hit rate: {:.1%};"
    977            " miss rate: {:.1%}".format(*self.hit_rates())
    978        )
    979 
    980    def hit_rates(self):
    981        direct = self._values["cache_hit_direct"]
    982        preprocessed = self._values["cache_hit_preprocessed"]
    983        miss = self._values["cache_miss"]
    984        total = float(direct + preprocessed + miss)
    985 
    986        if total > 0:
    987            direct /= total
    988            preprocessed /= total
    989            miss /= total
    990 
    991        return (direct, preprocessed, miss)
    992 
    993    def __sub__(self, other):
    994        result = CCacheStats()
    995 
    996        for k, prefix in self.STATS_KEYS:
    997            if k not in self._values and k not in other._values:
    998                continue
    999 
   1000            our_value = self._values.get(k, 0)
   1001            other_value = other._values.get(k, 0)
   1002 
   1003            if k in self.ABSOLUTE_KEYS:
   1004                result._values[k] = our_value
   1005            else:
   1006                result._values[k] = our_value - other_value
   1007 
   1008        return result
   1009 
   1010    def __str__(self):
   1011        LEFT_ALIGN = 34
   1012        lines = []
   1013 
   1014        for stat_key, stat_description in self.STATS_KEYS:
   1015            if stat_key not in self._values:
   1016                continue
   1017 
   1018            value = self._values[stat_key]
   1019 
   1020            if stat_key in self.FORMAT_KEYS:
   1021                value = "%15s" % self._format_value(value)
   1022            else:
   1023                value = "%8u" % value
   1024 
   1025            if isinstance(stat_description, tuple):
   1026                stat_description = stat_description[0]
   1027 
   1028            lines.append("%s%s" % (stat_description.ljust(LEFT_ALIGN), value))
   1029 
   1030        return "\n".join(lines)
   1031 
   1032    def __nonzero__(self):
   1033        relative_values = [
   1034            v for k, v in self._values.items() if k not in self.ABSOLUTE_KEYS
   1035        ]
   1036        return all(v >= 0 for v in relative_values) and any(
   1037            v > 0 for v in relative_values
   1038        )
   1039 
   1040    def __bool__(self):
   1041        return self.__nonzero__()
   1042 
   1043    @staticmethod
   1044    def _format_value(v):
   1045        if v > CCacheStats.GiB:
   1046            return "%.1f Gbytes" % (float(v) / CCacheStats.GiB)
   1047        elif v > CCacheStats.MiB:
   1048            return "%.1f Mbytes" % (float(v) / CCacheStats.MiB)
   1049        else:
   1050            return "%.1f Kbytes" % (float(v) / CCacheStats.KiB)
   1051 
   1052    @staticmethod
   1053    def check_version_3_7_or_newer(ccache):
   1054        output_version = subprocess.check_output(
   1055            [ccache, "--version"], universal_newlines=True
   1056        )
   1057        return CCacheStats._is_version_3_7_or_newer(output_version)
   1058 
   1059    @staticmethod
   1060    def _is_version_3_7_or_newer(output):
   1061        if "ccache version" not in output:
   1062            return False
   1063 
   1064        major = 0
   1065        minor = 0
   1066 
   1067        for line in output.splitlines():
   1068            version = re.search(r"ccache version (\d+).(\d+).*", line)
   1069            if version:
   1070                major = int(version.group(1))
   1071                minor = int(version.group(2))
   1072                break
   1073 
   1074        return ((major << 8) + minor) >= ((3 << 8) + 7)
   1075 
   1076 
   1077 class BuildDriver(MozbuildObject):
   1078    """Provides a high-level API for build actions."""
   1079 
   1080    def __init__(self, *args, **kwargs):
   1081        MozbuildObject.__init__(self, *args, virtualenv_name="build", **kwargs)
   1082        self.metrics = None
   1083        self.mach_context = None
   1084 
   1085    def build(
   1086        self,
   1087        metrics,
   1088        what=None,
   1089        jobs=0,
   1090        job_size=0,
   1091        directory=None,
   1092        verbose=False,
   1093        keep_going=False,
   1094        mach_context=None,
   1095        append_env=None,
   1096    ):
   1097        warnings_path = self._get_state_filename("warnings.json")
   1098        monitor = self._spawn(BuildMonitor)
   1099        monitor.init(warnings_path, self.log_manager.terminal, metrics)
   1100        status = self._build(
   1101            monitor,
   1102            metrics,
   1103            what,
   1104            jobs,
   1105            job_size,
   1106            directory,
   1107            verbose,
   1108            keep_going,
   1109            mach_context,
   1110            append_env,
   1111        )
   1112 
   1113        record_usage = True
   1114 
   1115        # On automation, only record usage for plain `mach build`
   1116        if "MOZ_AUTOMATION" in os.environ and what:
   1117            record_usage = False
   1118 
   1119        if record_usage:
   1120            monitor.record_usage()
   1121 
   1122        return status
   1123 
   1124    def _build(
   1125        self,
   1126        monitor,
   1127        metrics,
   1128        what=None,
   1129        jobs=0,
   1130        job_size=0,
   1131        directory=None,
   1132        verbose=False,
   1133        keep_going=False,
   1134        mach_context=None,
   1135        append_env=None,
   1136    ):
   1137        """Invoke the build backend.
   1138 
   1139        ``what`` defines the thing to build. If not defined, the default
   1140        target is used.
   1141        """
   1142        self.metrics = metrics
   1143        self.mach_context = mach_context
   1144        footer = BuildProgressFooter(self.log_manager.terminal, monitor)
   1145 
   1146        # Disable indexing in objdir because it is not necessary and can slow
   1147        # down builds.
   1148        mkdir(self.topobjdir, not_indexed=True)
   1149 
   1150        with BuildOutputManager(self.log_manager, monitor, footer) as output:
   1151            monitor.start()
   1152 
   1153            if directory is not None and not what:
   1154                print("Can only use -C/--directory with an explicit target name.")
   1155                return 1
   1156 
   1157            if directory is not None:
   1158                directory = mozpath.normsep(directory)
   1159                if directory.startswith("/"):
   1160                    directory = directory[1:]
   1161 
   1162            monitor.start_resource_recording()
   1163 
   1164            if self._check_clobber(self.mozconfig, os.environ):
   1165                return 1
   1166 
   1167            self.mach_context.command_attrs["clobber"] = False
   1168            self.metrics.mozbuild.clobber.set(False)
   1169            config = None
   1170            try:
   1171                config = self.config_environment
   1172            except Exception:
   1173                # If we don't already have a config environment this is either
   1174                # a fresh objdir or $OBJDIR/config.status has been removed for
   1175                # some reason, which indicates a clobber of sorts.
   1176                self.mach_context.command_attrs["clobber"] = True
   1177                self.metrics.mozbuild.clobber.set(True)
   1178 
   1179            # Record whether a clobber was requested so we can print
   1180            # a special message later if the build fails.
   1181            clobber_requested = False
   1182 
   1183            # Write out any changes to the current mozconfig in case
   1184            # they should invalidate configure.
   1185            self._write_mozconfig_json()
   1186 
   1187            previous_backend = None
   1188            if config is not None:
   1189                previous_backend = config.substs.get("BUILD_BACKENDS", [None])[0]
   1190 
   1191            config_rc = None
   1192            # Even if we have a config object, it may be out of date
   1193            # if something that influences its result has changed.
   1194            if config is None or self.build_out_of_date(
   1195                mozpath.join(self.topobjdir, "config.status"),
   1196                mozpath.join(self.topobjdir, "config_status_deps.in"),
   1197            ):
   1198                if previous_backend and "Make" not in previous_backend:
   1199                    clobber_requested = self._clobber_configure()
   1200 
   1201                if config is None:
   1202                    print(" Config object not found by mach.")
   1203 
   1204                config_rc = self.configure(
   1205                    metrics,
   1206                    buildstatus_messages=True,
   1207                    line_handler=output.on_line,
   1208                    append_env=append_env,
   1209                )
   1210 
   1211                if config_rc != 0:
   1212                    return config_rc
   1213 
   1214                config = self.reload_config_environment()
   1215 
   1216            if config.substs.get("MOZ_USING_CCACHE"):
   1217                ccache = config.substs.get("CCACHE")
   1218                ccache_start = monitor.ccache_stats(ccache)
   1219            else:
   1220                ccache_start = None
   1221 
   1222            # Collect glean metrics
   1223            substs = config.substs
   1224            mozbuild_metrics = metrics.mozbuild
   1225            mozbuild_metrics.compiler.set(substs.get("CC_TYPE", None))
   1226 
   1227            def get_substs_flag(name):
   1228                return bool(substs.get(name, None))
   1229 
   1230            host = substs.get("host")
   1231            monitor.resources.metadata["oscpu"] = host
   1232            target = substs.get("target")
   1233            if host != target:
   1234                monitor.resources.metadata["abi"] = target
   1235 
   1236            product_name = substs.get("MOZ_BUILD_APP")
   1237            app_displayname = substs.get("MOZ_APP_DISPLAYNAME")
   1238            if app_displayname:
   1239                product_name = app_displayname
   1240                app_version = substs.get("MOZ_APP_VERSION")
   1241                if app_version:
   1242                    product_name += " " + app_version
   1243            monitor.resources.metadata["product"] = product_name
   1244 
   1245            mozbuild_metrics.artifact.set(get_substs_flag("MOZ_ARTIFACT_BUILDS"))
   1246            mozbuild_metrics.debug.set(get_substs_flag("MOZ_DEBUG"))
   1247            mozbuild_metrics.opt.set(get_substs_flag("MOZ_OPTIMIZE"))
   1248            mozbuild_metrics.ccache.set(get_substs_flag("CCACHE"))
   1249            using_sccache = get_substs_flag("MOZ_USING_SCCACHE")
   1250            mozbuild_metrics.sccache.set(using_sccache)
   1251            mozbuild_metrics.icecream.set(get_substs_flag("CXX_IS_ICECREAM"))
   1252            mozbuild_metrics.project.set(substs.get("MOZ_BUILD_APP", ""))
   1253            mozbuild_metrics.target.set(target)
   1254 
   1255            all_backends = config.substs.get("BUILD_BACKENDS", [None])
   1256            active_backend = all_backends[0]
   1257 
   1258            status = None
   1259 
   1260            if not config_rc and any([
   1261                self.backend_out_of_date(
   1262                    mozpath.join(self.topobjdir, "backend.%sBackend" % backend)
   1263                )
   1264                for backend in all_backends
   1265            ]):
   1266                print("Build configuration changed. Regenerating backend.")
   1267                args = [
   1268                    config.substs["PYTHON3"],
   1269                    mozpath.join(self.topobjdir, "config.status"),
   1270                ]
   1271                self.run_process(args, cwd=self.topobjdir, pass_thru=True)
   1272 
   1273            if jobs == 0:
   1274                for param in self.mozconfig.get("make_extra") or []:
   1275                    key, value = param.split("=", 1)
   1276                    if key == "MOZ_PARALLEL_BUILD":
   1277                        jobs = int(value)
   1278 
   1279            if "Make" not in active_backend:
   1280                backend_cls = get_backend_class(active_backend)(config)
   1281                status = backend_cls.build(self, output, jobs, verbose, what)
   1282 
   1283                if status and clobber_requested:
   1284                    for line in CLOBBER_REQUESTED_MESSAGE.splitlines():
   1285                        self.log(
   1286                            logging.WARNING, "clobber", {"msg": line.rstrip()}, "{msg}"
   1287                        )
   1288 
   1289            if what and status is None:
   1290                # Collect target pairs.
   1291                target_pairs = []
   1292                for target in what:
   1293                    path_arg = self._wrap_path_argument(target)
   1294 
   1295                    if directory is not None:
   1296                        make_dir = mozpath.join(self.topobjdir, directory)
   1297                        make_target = target
   1298                    else:
   1299                        make_dir, make_target = resolve_target_to_make(
   1300                            self.topobjdir, path_arg.relpath()
   1301                        )
   1302 
   1303                    if make_dir is None and make_target is None:
   1304                        return 1
   1305 
   1306                    if config.is_artifact_build and target.startswith("installers-"):
   1307                        # See https://bugzilla.mozilla.org/show_bug.cgi?id=1387485
   1308                        print(
   1309                            "Localized Builds are not supported with Artifact Builds enabled.\n"
   1310                            "You should disable Artifact Builds (Use --disable-compile-environment "
   1311                            "in your mozconfig instead) then re-build to proceed."
   1312                        )
   1313                        return 1
   1314 
   1315                    # See bug 886162 - we don't want to "accidentally" build
   1316                    # the entire tree (if that's really the intent, it's
   1317                    # unlikely they would have specified a directory.)
   1318                    if not make_dir and not make_target:
   1319                        print(
   1320                            "The specified directory doesn't contain a "
   1321                            "Makefile and the first parent with one is the "
   1322                            "root of the tree. Please specify a directory "
   1323                            "with a Makefile or run |mach build| if you "
   1324                            "want to build the entire tree."
   1325                        )
   1326                        return 1
   1327 
   1328                    target_pairs.append((make_dir, make_target))
   1329 
   1330                # Build target pairs.
   1331                for make_dir, make_target in target_pairs:
   1332                    # We don't display build status messages during partial
   1333                    # tree builds because they aren't reliable there. This
   1334                    # could potentially be fixed if the build monitor were more
   1335                    # intelligent about encountering undefined state.
   1336                    no_build_status = "1" if make_dir is not None else ""
   1337                    tgt_env = dict(append_env or {})
   1338                    tgt_env["NO_BUILDSTATUS_MESSAGES"] = no_build_status
   1339                    status = self._run_make(
   1340                        directory=make_dir,
   1341                        target=make_target,
   1342                        line_handler=output.on_line,
   1343                        log=False,
   1344                        print_directory=False,
   1345                        ensure_exit_code=False,
   1346                        num_jobs=jobs,
   1347                        job_size=job_size,
   1348                        silent=not verbose,
   1349                        append_env=tgt_env,
   1350                        keep_going=keep_going,
   1351                    )
   1352 
   1353                    if status != 0:
   1354                        break
   1355 
   1356            elif status is None:
   1357                # If the backend doesn't specify a build() method, then just
   1358                # call client.mk directly.
   1359                status = self._run_client_mk(
   1360                    line_handler=output.on_line,
   1361                    jobs=jobs,
   1362                    job_size=job_size,
   1363                    verbose=verbose,
   1364                    keep_going=keep_going,
   1365                    append_env=append_env,
   1366                )
   1367 
   1368            self.log(
   1369                logging.WARNING,
   1370                "warning_summary",
   1371                {"count": len(monitor.warnings_database)},
   1372                "{count} compiler warnings present.",
   1373            )
   1374 
   1375            # Try to run the active build backend's post-build step, if possible.
   1376            try:
   1377                active_backend = config.substs.get("BUILD_BACKENDS", [None])[0]
   1378                if active_backend:
   1379                    backend_cls = get_backend_class(active_backend)(config)
   1380                    new_status = backend_cls.post_build(
   1381                        self, output, jobs, verbose, status
   1382                    )
   1383                    status = new_status
   1384            except Exception as ex:
   1385                self.log(
   1386                    logging.DEBUG,
   1387                    "post_build",
   1388                    {"ex": str(ex)},
   1389                    "Unable to run active build backend's post-build step; "
   1390                    + "failing the build due to exception: {ex}.",
   1391                )
   1392                if not status:
   1393                    # If the underlying build provided a failing status, pass
   1394                    # it through; otherwise, fail.
   1395                    status = 1
   1396 
   1397            monitor.finish()
   1398 
   1399        if status == 0:
   1400            usage = monitor.get_resource_usage()
   1401            if usage:
   1402                self.mach_context.command_attrs["usage"] = usage
   1403                monitor.log_resource_usage(usage)
   1404 
   1405        # Print the collected compiler warnings. This is redundant with
   1406        # inline output from the compiler itself. However, unlike inline
   1407        # output, this list is sorted and grouped by file, making it
   1408        # easier to triage output.
   1409        #
   1410        # Only do this if we had a successful build. If the build failed,
   1411        # there are more important things in the log to look for than
   1412        # whatever code we warned about.
   1413        if not status:
   1414            # Suppress warnings for 3rd party projects in local builds
   1415            # until we suppress them for real.
   1416            # TODO remove entries/feature once we stop generating warnings
   1417            # in these directories.
   1418            pathToThirdparty = mozpath.join(
   1419                self.topsrcdir, "tools", "rewriting", "ThirdPartyPaths.txt"
   1420            )
   1421 
   1422            pathToGenerated = mozpath.join(
   1423                self.topsrcdir, "tools", "rewriting", "Generated.txt"
   1424            )
   1425 
   1426            if os.path.exists(pathToThirdparty):
   1427                with open(pathToThirdparty, encoding="utf-8", newline="\n") as f, open(
   1428                    pathToGenerated, encoding="utf-8", newline="\n"
   1429                ) as g:
   1430                    # Normalize the path (no trailing /)
   1431                    LOCAL_SUPPRESS_DIRS = tuple(
   1432                        [line.strip("\n/") for line in f]
   1433                        + [line.strip("\n/") for line in g]
   1434                    )
   1435            else:
   1436                # For application based on gecko like thunderbird
   1437                LOCAL_SUPPRESS_DIRS = ()
   1438 
   1439            suppressed_by_dir = Counter()
   1440 
   1441            THIRD_PARTY_CODE = "third-party code"
   1442            suppressed = set(
   1443                w.replace("-Wno-error=", "-W")
   1444                for w in substs.get("WARNINGS_CFLAGS", [])
   1445                + substs.get("WARNINGS_CXXFLAGS", [])
   1446                if w.startswith("-Wno-error=")
   1447            )
   1448            warnings = []
   1449            for warning in sorted(monitor.instance_warnings):
   1450                path = mozpath.normsep(warning["filename"])
   1451                if path.startswith(self.topsrcdir):
   1452                    path = path[len(self.topsrcdir) + 1 :]
   1453 
   1454                warning["normpath"] = path
   1455 
   1456                if "MOZ_AUTOMATION" not in os.environ:
   1457                    if path.startswith(LOCAL_SUPPRESS_DIRS):
   1458                        suppressed_by_dir[THIRD_PARTY_CODE] += 1
   1459                        continue
   1460 
   1461                    if warning["flag"] in suppressed:
   1462                        suppressed_by_dir[mozpath.dirname(path)] += 1
   1463                        continue
   1464 
   1465                warnings.append(warning)
   1466 
   1467            if THIRD_PARTY_CODE in suppressed_by_dir:
   1468                suppressed_third_party_code = [
   1469                    (THIRD_PARTY_CODE, suppressed_by_dir.pop(THIRD_PARTY_CODE))
   1470                ]
   1471            else:
   1472                suppressed_third_party_code = []
   1473            for d, count in suppressed_third_party_code + sorted(
   1474                suppressed_by_dir.items()
   1475            ):
   1476                self.log(
   1477                    logging.WARNING,
   1478                    "suppressed_warning",
   1479                    {"dir": d, "count": count},
   1480                    "(suppressed {count} warnings in {dir})",
   1481                )
   1482 
   1483            for warning in warnings:
   1484                if warning["column"] is not None:
   1485                    self.log(
   1486                        logging.WARNING,
   1487                        "compiler_warning",
   1488                        warning,
   1489                        "warning: {normpath}:{line}:{column} [{flag}] {message}",
   1490                    )
   1491                else:
   1492                    self.log(
   1493                        logging.WARNING,
   1494                        "compiler_warning",
   1495                        warning,
   1496                        "warning: {normpath}:{line} [{flag}] {message}",
   1497                    )
   1498 
   1499        high_finder, finder_percent = monitor.have_high_finder_usage()
   1500        if high_finder:
   1501            print(FINDER_SLOW_MESSAGE % finder_percent)
   1502 
   1503        if config.substs.get("MOZ_USING_CCACHE"):
   1504            ccache_end = monitor.ccache_stats(ccache)
   1505        else:
   1506            ccache_end = None
   1507 
   1508        ccache_diff = None
   1509        if ccache_start and ccache_end:
   1510            ccache_diff = ccache_end - ccache_start
   1511            if ccache_diff:
   1512                self.log(
   1513                    logging.INFO,
   1514                    "ccache",
   1515                    {"msg": ccache_diff.hit_rate_message()},
   1516                    "{msg}",
   1517                )
   1518 
   1519        notify_minimum_time = 300
   1520        try:
   1521            notify_minimum_time = int(os.environ.get("MACH_NOTIFY_MINTIME", "300"))
   1522        except ValueError:
   1523            # Just stick with the default
   1524            pass
   1525 
   1526        if monitor.elapsed > notify_minimum_time:
   1527            # Display a notification when the build completes.
   1528            self.notify("Build complete" if not status else "Build failed")
   1529 
   1530        if status:
   1531            if what and any([
   1532                target for target in what if target not in ("faster", "binaries")
   1533            ]):
   1534                print(
   1535                    "Hey! Builds initiated with `mach build "
   1536                    "$A_SPECIFIC_TARGET` may not always work, even if the "
   1537                    "code being built is correct. Consider doing a bare "
   1538                    "`mach build` instead."
   1539                )
   1540            return status
   1541 
   1542        if monitor.have_resource_usage:
   1543            excessive, swap_in, swap_out = monitor.have_excessive_swapping()
   1544            # if excessive:
   1545            #    print(EXCESSIVE_SWAP_MESSAGE)
   1546 
   1547            print("To view a profile of the build, run |mach resource-usage|.")
   1548 
   1549        long_build = monitor.elapsed > 1200
   1550 
   1551        if long_build:
   1552            output.on_line(
   1553                "We know it took a while, but your build finally finished successfully!"
   1554            )
   1555            if not using_sccache:
   1556                output.on_line(
   1557                    "If you are building Firefox often, SCCache can save you a lot "
   1558                    "of time. You can learn more here: "
   1559                    "https://firefox-source-docs.mozilla.org/setup/"
   1560                    "configuring_build_options.html#sccache"
   1561                )
   1562        else:
   1563            output.on_line("Your build was successful!")
   1564 
   1565        # Only for full builds because incremental builders likely don't
   1566        # need to be burdened with this.
   1567        if not what:
   1568            try:
   1569                # Fennec doesn't have useful output from just building. We should
   1570                # arguably make the build action useful for Fennec. Another day...
   1571                if self.substs["MOZ_BUILD_APP"] != "mobile/android":
   1572                    print("To take your build for a test drive, run: |mach run|")
   1573                app = self.substs["MOZ_BUILD_APP"]
   1574                if app in ("browser", "mobile/android"):
   1575                    print(
   1576                        "For more information on what to do now, see "
   1577                        "https://firefox-source-docs.mozilla.org/setup/contributing_code.html"  # noqa
   1578                    )
   1579            except Exception:
   1580                # Ignore Exceptions in case we can't find config.status (such
   1581                # as when doing OSX Universal builds)
   1582                pass
   1583 
   1584        return status
   1585 
   1586    def configure(
   1587        self,
   1588        metrics,
   1589        options=None,
   1590        buildstatus_messages=False,
   1591        line_handler=None,
   1592        append_env=None,
   1593    ):
   1594        # Disable indexing in objdir because it is not necessary and can slow
   1595        # down builds.
   1596        self.metrics = metrics
   1597        mkdir(self.topobjdir, not_indexed=True)
   1598        self._write_mozconfig_json()
   1599 
   1600        def on_line(line):
   1601            self.log(logging.INFO, "build_output", {"line": line}, "{line}")
   1602 
   1603        line_handler = line_handler or on_line
   1604 
   1605        append_env = dict(append_env or {})
   1606 
   1607        # Back when client.mk was used, `mk_add_options "export ..."` lines
   1608        # from the mozconfig would spill into the configure environment, so
   1609        # add that for backwards compatibility.
   1610        for line in self.mozconfig["make_extra"] or []:
   1611            if line.startswith("export "):
   1612                k, eq, v = line[len("export ") :].partition("=")
   1613                if eq == "=":
   1614                    append_env[k] = v
   1615 
   1616        build_site = CommandSiteManager.from_environment(
   1617            self.topsrcdir,
   1618            lambda: get_state_dir(specific_to_topsrcdir=True, topsrcdir=self.topsrcdir),
   1619            "build",
   1620            get_virtualenv_base_dir(self.topsrcdir),
   1621        )
   1622        build_site.ensure()
   1623 
   1624        command = [build_site.python_path, mozpath.join(self.topsrcdir, "configure.py")]
   1625        if options:
   1626            command.extend(options)
   1627 
   1628        if buildstatus_messages:
   1629            append_env["MOZ_CONFIGURE_BUILDSTATUS"] = "1"
   1630            line_handler("BUILDSTATUS TIERS configure")
   1631            line_handler("BUILDSTATUS TIER_START configure")
   1632 
   1633        env = os.environ.copy()
   1634        env.update(append_env)
   1635 
   1636        with subprocess.Popen(
   1637            command,
   1638            cwd=self.topobjdir,
   1639            env=env,
   1640            stdout=subprocess.PIPE,
   1641            stderr=subprocess.STDOUT,
   1642            universal_newlines=True,
   1643        ) as process:
   1644            for line in process.stdout:
   1645                line_handler(line.rstrip())
   1646            status = process.wait()
   1647        if buildstatus_messages:
   1648            line_handler("BUILDSTATUS TIER_FINISH configure")
   1649        if status:
   1650            print('*** Fix above errors and then restart with "./mach build"')
   1651        else:
   1652            print("Configure complete!")
   1653            print("Be sure to run |mach build| to pick up any changes")
   1654 
   1655        return status
   1656 
   1657    def install_tests(self):
   1658        """Install test files."""
   1659 
   1660        if self.is_clobber_needed():
   1661            print(
   1662                INSTALL_TESTS_CLOBBER.format(
   1663                    clobber_file=mozpath.join(self.topobjdir, "CLOBBER")
   1664                )
   1665            )
   1666            sys.exit(1)
   1667 
   1668        install_test_files(mozpath.normpath(self.topsrcdir), self.topobjdir, "_tests")
   1669 
   1670    def _clobber_configure(self):
   1671        # This is an optimistic treatment of the CLOBBER file for when we have
   1672        # some trust in the build system: an update to the CLOBBER file is
   1673        # interpreted to mean that configure will fail during an incremental
   1674        # build, which is handled by removing intermediate configure artifacts
   1675        # and subsections of the objdir related to python and testing before
   1676        # proceeding.
   1677        clobberer = Clobberer(self.topsrcdir, self.topobjdir)
   1678        clobber_output = io.StringIO()
   1679        res = clobberer.maybe_do_clobber(os.getcwd(), False, clobber_output)
   1680        required, performed, message = res
   1681        assert not performed
   1682        if not required:
   1683            return False
   1684 
   1685        def remove_objdir_path(path):
   1686            path = mozpath.join(self.topobjdir, path)
   1687            self.log(
   1688                logging.WARNING,
   1689                "clobber",
   1690                {"path": path},
   1691                "CLOBBER file has been updated, removing {path}.",
   1692            )
   1693            mozfile.remove(path)
   1694 
   1695        # Remove files we think could cause "configure" clobber bugs.
   1696        for f in ("old-configure.vars", "config.cache", "configure.pkl"):
   1697            remove_objdir_path(f)
   1698            remove_objdir_path(mozpath.join("js", "src", f))
   1699 
   1700        rm_dirs = [
   1701            # Stale paths in our virtualenv may cause build-backend
   1702            # to fail.
   1703            "_virtualenvs",
   1704            # Some tests may accumulate state in the objdir that may
   1705            # become invalid after srcdir changes.
   1706            "_tests",
   1707        ]
   1708 
   1709        for d in rm_dirs:
   1710            remove_objdir_path(d)
   1711 
   1712        os.utime(mozpath.join(self.topobjdir, "CLOBBER"), None)
   1713        return True
   1714 
   1715    def _write_mozconfig_json(self):
   1716        mozconfig_json = mozpath.join(self.topobjdir, ".mozconfig.json")
   1717        with FileAvoidWrite(mozconfig_json) as fh:
   1718            to_write = json.dumps(
   1719                {
   1720                    "topsrcdir": self.topsrcdir,
   1721                    "topobjdir": self.topobjdir,
   1722                    "mozconfig": self.mozconfig,
   1723                },
   1724                sort_keys=True,
   1725                indent=2,
   1726            )
   1727 
   1728            # json.dumps in python2 inserts some trailing whitespace while
   1729            # json.dumps in python3 does not, which defeats the FileAvoidWrite
   1730            # mechanism. Strip the trailing whitespace to avoid rewriting this
   1731            # file unnecessarily.
   1732            to_write = "\n".join([line.rstrip() for line in to_write.splitlines()])
   1733            fh.write(to_write)
   1734 
   1735    def _run_client_mk(
   1736        self,
   1737        target=None,
   1738        line_handler=None,
   1739        jobs=0,
   1740        job_size=0,
   1741        verbose=None,
   1742        keep_going=False,
   1743        append_env=None,
   1744    ):
   1745        append_env = dict(append_env or {})
   1746        append_env["TOPSRCDIR"] = self.topsrcdir
   1747 
   1748        append_env["CONFIG_GUESS"] = self.resolve_config_guess()
   1749 
   1750        mozconfig = self.mozconfig
   1751 
   1752        mozconfig_make_lines = []
   1753        for arg in mozconfig["make_extra"] or []:
   1754            mozconfig_make_lines.append(arg)
   1755 
   1756        if mozconfig["make_flags"]:
   1757            mozconfig_make_lines.append(
   1758                "MOZ_MAKE_FLAGS=%s" % " ".join(mozconfig["make_flags"])
   1759            )
   1760        objdir = mozpath.normsep(self.topobjdir)
   1761        mozconfig_make_lines.append("MOZ_OBJDIR=%s" % objdir)
   1762        mozconfig_make_lines.append("OBJDIR=%s" % objdir)
   1763 
   1764        if mozconfig["path"]:
   1765            mozconfig_make_lines.append(
   1766                "FOUND_MOZCONFIG=%s" % mozpath.normsep(mozconfig["path"])
   1767            )
   1768            mozconfig_make_lines.append("export FOUND_MOZCONFIG")
   1769 
   1770        # The .mozconfig.mk file only contains exported variables and lines with
   1771        # UPLOAD_EXTRA_FILES.
   1772        mozconfig_filtered_lines = [
   1773            line
   1774            for line in mozconfig_make_lines
   1775            # Bug 1418122 investigate why UPLOAD_EXTRA_FILES is special and
   1776            # remove it.
   1777            if line.startswith("export ") or "UPLOAD_EXTRA_FILES" in line
   1778        ]
   1779 
   1780        mozconfig_client_mk = mozpath.join(self.topobjdir, ".mozconfig-client-mk")
   1781        with FileAvoidWrite(mozconfig_client_mk) as fh:
   1782            fh.write("\n".join(mozconfig_make_lines))
   1783 
   1784        mozconfig_mk = mozpath.join(self.topobjdir, ".mozconfig.mk")
   1785        with FileAvoidWrite(mozconfig_mk) as fh:
   1786            fh.write("\n".join(mozconfig_filtered_lines))
   1787 
   1788        # Copy the original mozconfig to the objdir.
   1789        mozconfig_objdir = mozpath.join(self.topobjdir, ".mozconfig")
   1790        if mozconfig["path"]:
   1791            with open(mozconfig["path"]) as ifh:
   1792                with FileAvoidWrite(mozconfig_objdir) as ofh:
   1793                    ofh.write(ifh.read())
   1794        else:
   1795            try:
   1796                os.unlink(mozconfig_objdir)
   1797            except OSError as e:
   1798                if e.errno != errno.ENOENT:
   1799                    raise
   1800 
   1801        if mozconfig_make_lines:
   1802            self.log(
   1803                logging.WARNING,
   1804                "mozconfig_content",
   1805                {
   1806                    "path": mozconfig["path"],
   1807                    "content": "\n    ".join(mozconfig_make_lines),
   1808                },
   1809                "Adding make options from {path}\n    {content}",
   1810            )
   1811 
   1812        append_env["OBJDIR"] = mozpath.normsep(self.topobjdir)
   1813 
   1814        return self._run_make(
   1815            srcdir=True,
   1816            filename="client.mk",
   1817            ensure_exit_code=False,
   1818            print_directory=False,
   1819            target=target,
   1820            line_handler=line_handler,
   1821            log=False,
   1822            num_jobs=jobs,
   1823            job_size=job_size,
   1824            silent=not verbose,
   1825            keep_going=keep_going,
   1826            append_env=append_env,
   1827        )
   1828 
   1829    def _check_clobber(self, mozconfig, env):
   1830        """Run `Clobberer.maybe_do_clobber`, log the result and return a status bool.
   1831 
   1832        Wraps the clobbering logic in `Clobberer.maybe_do_clobber` to provide logging
   1833        and handling of the `AUTOCLOBBER` mozconfig option.
   1834 
   1835        Return a bool indicating whether the clobber reached an error state. For example,
   1836        return `True` if the clobber was required but not completed, and return `False` if
   1837        the clobber was not required and not completed.
   1838        """
   1839        auto_clobber = any([
   1840            env.get("AUTOCLOBBER", False),
   1841            (mozconfig["env"] or {}).get("added", {}).get("AUTOCLOBBER", False),
   1842            "AUTOCLOBBER=1" in (mozconfig["make_extra"] or []),
   1843        ])
   1844        from mozbuild.base import BuildEnvironmentNotFoundException
   1845 
   1846        substs = dict()
   1847        try:
   1848            substs = self.substs
   1849        except BuildEnvironmentNotFoundException:
   1850            # We'll just use an empty substs if there is no config.
   1851            pass
   1852        clobberer = Clobberer(self.topsrcdir, self.topobjdir, substs)
   1853        clobber_output = io.StringIO()
   1854        res = clobberer.maybe_do_clobber(os.getcwd(), auto_clobber, clobber_output)
   1855        clobber_output.seek(0)
   1856        for line in clobber_output.readlines():
   1857            self.log(logging.WARNING, "clobber", {"msg": line.rstrip()}, "{msg}")
   1858 
   1859        clobber_required, clobber_performed, clobber_message = res
   1860        if clobber_required and not clobber_performed:
   1861            for line in clobber_message.splitlines():
   1862                self.log(logging.WARNING, "clobber", {"msg": line.rstrip()}, "{msg}")
   1863            return True
   1864 
   1865        if clobber_performed and env.get("TINDERBOX_OUTPUT"):
   1866            self.log(
   1867                logging.WARNING,
   1868                "clobber",
   1869                {"msg": "TinderboxPrint: auto clobber"},
   1870                "{msg}",
   1871            )
   1872 
   1873        return False