summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozbuild/controller/building.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/mozbuild/mozbuild/controller/building.py')
-rw-r--r--python/mozbuild/mozbuild/controller/building.py1872
1 files changed, 1872 insertions, 0 deletions
diff --git a/python/mozbuild/mozbuild/controller/building.py b/python/mozbuild/mozbuild/controller/building.py
new file mode 100644
index 0000000000..de6c01afe4
--- /dev/null
+++ b/python/mozbuild/mozbuild/controller/building.py
@@ -0,0 +1,1872 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import errno
+import getpass
+import io
+import json
+import logging
+import os
+import re
+import shutil
+import subprocess
+import sys
+import time
+from collections import Counter, OrderedDict, namedtuple
+from textwrap import TextWrapper
+
+import six
+from mach.site import CommandSiteManager
+
+try:
+ import psutil
+except Exception:
+ psutil = None
+
+import mozfile
+import mozpack.path as mozpath
+from mach.mixin.logging import LoggingMixin
+from mach.util import get_state_dir
+from mozsystemmonitor.resourcemonitor import SystemResourceMonitor
+from mozterm.widgets import Footer
+
+from ..backend import get_backend_class
+from ..base import MozbuildObject
+from ..compilation.warnings import WarningsCollector, WarningsDatabase
+from ..testing import install_test_files
+from ..util import FileAvoidWrite, mkdir, resolve_target_to_make
+from .clobber import Clobberer
+
+FINDER_SLOW_MESSAGE = """
+===================
+PERFORMANCE WARNING
+
+The OS X Finder application (file indexing used by Spotlight) used a lot of CPU
+during the build - an average of %f%% (100%% is 1 core). This made your build
+slower.
+
+Consider adding ".noindex" to the end of your object directory name to have
+Finder ignore it. Or, add an indexing exclusion through the Spotlight System
+Preferences.
+===================
+""".strip()
+
+
+INSTALL_TESTS_CLOBBER = "".join(
+ [
+ TextWrapper().fill(line) + "\n"
+ for line in """
+The build system was unable to install tests because the CLOBBER file has \
+been updated. This means if you edited any test files, your changes may not \
+be picked up until a full/clobber build is performed.
+
+The easiest and fastest way to perform a clobber build is to run:
+
+ $ mach clobber
+ $ mach build
+
+If you did not modify any test files, it is safe to ignore this message \
+and proceed with running tests. To do this run:
+
+ $ touch {clobber_file}
+""".splitlines()
+ ]
+)
+
+CLOBBER_REQUESTED_MESSAGE = """
+===================
+The CLOBBER file was updated prior to this build. A clobber build may be
+required to succeed, but we weren't expecting it to.
+
+Please consider filing a bug for this failure if you have reason to believe
+this is a clobber bug and not due to local changes.
+===================
+""".strip()
+
+
+BuildOutputResult = namedtuple(
+ "BuildOutputResult", ("warning", "state_changed", "message")
+)
+
+
+class TierStatus(object):
+ """Represents the state and progress of tier traversal.
+
+ The build system is organized into linear phases called tiers. Each tier
+ executes in the order it was defined, 1 at a time.
+ """
+
+ def __init__(self, resources):
+ """Accepts a SystemResourceMonitor to record results against."""
+ self.tiers = OrderedDict()
+ self.tier_status = OrderedDict()
+ self.resources = resources
+
+ def set_tiers(self, tiers):
+ """Record the set of known tiers."""
+ for tier in tiers:
+ self.tiers[tier] = dict(
+ begin_time=None,
+ finish_time=None,
+ duration=None,
+ )
+ self.tier_status[tier] = None
+
+ def begin_tier(self, tier):
+ """Record that execution of a tier has begun."""
+ self.tier_status[tier] = "active"
+ t = self.tiers[tier]
+ t["begin_time"] = time.monotonic()
+ self.resources.begin_phase(tier)
+
+ def finish_tier(self, tier):
+ """Record that execution of a tier has finished."""
+ self.tier_status[tier] = "finished"
+ t = self.tiers[tier]
+ t["finish_time"] = time.monotonic()
+ t["duration"] = self.resources.finish_phase(tier)
+
+ def tiered_resource_usage(self):
+ """Obtains an object containing resource usage for tiers.
+
+ The returned object is suitable for serialization.
+ """
+ o = []
+
+ for tier, state in self.tiers.items():
+ t_entry = dict(
+ name=tier,
+ start=state["begin_time"],
+ end=state["finish_time"],
+ duration=state["duration"],
+ )
+
+ self.add_resources_to_dict(t_entry, phase=tier)
+
+ o.append(t_entry)
+
+ return o
+
+ def add_resources_to_dict(self, entry, start=None, end=None, phase=None):
+ """Helper function to append resource information to a dict."""
+ cpu_percent = self.resources.aggregate_cpu_percent(
+ start=start, end=end, phase=phase, per_cpu=False
+ )
+ cpu_times = self.resources.aggregate_cpu_times(
+ start=start, end=end, phase=phase, per_cpu=False
+ )
+ io = self.resources.aggregate_io(start=start, end=end, phase=phase)
+
+ if cpu_percent is None:
+ return entry
+
+ entry["cpu_percent"] = cpu_percent
+ entry["cpu_times"] = list(cpu_times)
+ entry["io"] = list(io)
+
+ return entry
+
+ def add_resource_fields_to_dict(self, d):
+ for usage in self.resources.range_usage():
+ cpu_times = self.resources.aggregate_cpu_times(per_cpu=False)
+
+ d["cpu_times_fields"] = list(cpu_times._fields)
+ d["io_fields"] = list(usage.io._fields)
+ d["virt_fields"] = list(usage.virt._fields)
+ d["swap_fields"] = list(usage.swap._fields)
+
+ return d
+
+
+class BuildMonitor(MozbuildObject):
+ """Monitors the output of the build."""
+
+ def init(self, warnings_path):
+ """Create a new monitor.
+
+ warnings_path is a path of a warnings database to use.
+ """
+ self._warnings_path = warnings_path
+ self.resources = SystemResourceMonitor(poll_interval=1.0)
+ self._resources_started = False
+
+ self.tiers = TierStatus(self.resources)
+
+ self.warnings_database = WarningsDatabase()
+ if os.path.exists(warnings_path):
+ try:
+ self.warnings_database.load_from_file(warnings_path)
+ except ValueError:
+ os.remove(warnings_path)
+
+ # Contains warnings unique to this invocation. Not populated with old
+ # warnings.
+ self.instance_warnings = WarningsDatabase()
+
+ def on_warning(warning):
+ # Skip `errors`
+ if warning["type"] == "error":
+ return
+
+ filename = warning["filename"]
+
+ if not os.path.exists(filename):
+ raise Exception("Could not find file containing warning: %s" % filename)
+
+ self.warnings_database.insert(warning)
+ # Make a copy so mutations don't impact other database.
+ self.instance_warnings.insert(warning.copy())
+
+ self._warnings_collector = WarningsCollector(on_warning, objdir=self.topobjdir)
+ self._build_tasks = []
+
+ self.build_objects = []
+ self.build_dirs = set()
+
+ def start(self):
+ """Record the start of the build."""
+ self.start_time = time.monotonic()
+ self._finder_start_cpu = self._get_finder_cpu_usage()
+
+ def start_resource_recording(self):
+ # This should be merged into start() once bug 892342 lands.
+ self.resources.start()
+ self._resources_started = True
+
+ def on_line(self, line):
+ """Consume a line of output from the build system.
+
+ This will parse the line for state and determine whether more action is
+ needed.
+
+ Returns a BuildOutputResult instance.
+
+ In this named tuple, warning will be an object describing a new parsed
+ warning. Otherwise it will be None.
+
+ state_changed indicates whether the build system changed state with
+ this line. If the build system changed state, the caller may want to
+ query this instance for the current state in order to update UI, etc.
+
+ message is either None, or the content of a message to be
+ displayed to the user.
+ """
+ message = None
+
+ if line.startswith("BUILDSTATUS"):
+ args = line.split()[1:]
+
+ action = args.pop(0)
+ update_needed = True
+
+ if action == "TIERS":
+ self.tiers.set_tiers(args)
+ update_needed = False
+ elif action == "TIER_START":
+ tier = args[0]
+ self.tiers.begin_tier(tier)
+ elif action == "TIER_FINISH":
+ (tier,) = args
+ self.tiers.finish_tier(tier)
+ elif action == "OBJECT_FILE":
+ self.build_objects.append(args[0])
+ update_needed = False
+ elif action == "BUILD_VERBOSE":
+ build_dir = args[0]
+ if build_dir not in self.build_dirs:
+ self.build_dirs.add(build_dir)
+ message = build_dir
+ update_needed = False
+ else:
+ raise Exception("Unknown build status: %s" % action)
+
+ return BuildOutputResult(None, update_needed, message)
+ elif line.startswith("BUILDTASK"):
+ _, data = line.split(maxsplit=1)
+ # Check that we can parse the JSON. Skip this line if we can't;
+ # we'll be missing data, but that's not a huge deal.
+ try:
+ json.loads(data)
+ self._build_tasks.append(data)
+ except json.decoder.JSONDecodeError:
+ pass
+ return BuildOutputResult(None, False, None)
+
+ warning = None
+
+ try:
+ warning = self._warnings_collector.process_line(line)
+ message = line
+ except Exception:
+ pass
+
+ return BuildOutputResult(warning, False, message)
+
+ def stop_resource_recording(self):
+ if self._resources_started:
+ self.resources.stop()
+
+ self._resources_started = False
+
+ def finish(self, record_usage=True):
+ """Record the end of the build."""
+ self.stop_resource_recording()
+ self.end_time = time.monotonic()
+ self._finder_end_cpu = self._get_finder_cpu_usage()
+ self.elapsed = self.end_time - self.start_time
+
+ self.warnings_database.prune()
+ self.warnings_database.save_to_file(self._warnings_path)
+
+ if "MOZ_AUTOMATION" not in os.environ:
+ build_tasks_path = self._get_state_filename("build_tasks.json")
+ with io.open(build_tasks_path, "w", encoding="utf-8", newline="\n") as fh:
+ fh.write("[")
+ first = True
+ for task in self._build_tasks:
+ # We've already verified all of these are valid JSON, so we
+ # can write the data out to the file directly.
+ fh.write("%s\n %s" % ("," if not first else "", task))
+ first = False
+ fh.write("\n]\n")
+
+ # Record usage.
+ if not record_usage:
+ return
+
+ try:
+ usage = self.get_resource_usage()
+ if not usage:
+ return
+
+ self.log_resource_usage(usage)
+ # When running on automation, we store the resource usage data in
+ # the upload path, alongside, for convenience, a copy of the HTML
+ # viewer.
+ if "MOZ_AUTOMATION" in os.environ and "UPLOAD_PATH" in os.environ:
+ build_resources_path = os.path.join(
+ os.environ["UPLOAD_PATH"], "build_resources.json"
+ )
+ shutil.copy(
+ os.path.join(
+ self.topsrcdir,
+ "python",
+ "mozbuild",
+ "mozbuild",
+ "resources",
+ "html-build-viewer",
+ "build_resources.html",
+ ),
+ os.environ["UPLOAD_PATH"],
+ )
+ else:
+ build_resources_path = self._get_state_filename("build_resources.json")
+ with io.open(
+ build_resources_path, "w", encoding="utf-8", newline="\n"
+ ) as fh:
+ to_write = six.ensure_text(
+ json.dumps(self.resources.as_dict(), indent=2)
+ )
+ fh.write(to_write)
+ except Exception as e:
+ self.log(
+ logging.WARNING,
+ "build_resources_error",
+ {"msg": str(e)},
+ "Exception when writing resource usage file: {msg}",
+ )
+
+ def _get_finder_cpu_usage(self):
+ """Obtain the CPU usage of the Finder app on OS X.
+
+ This is used to detect high CPU usage.
+ """
+ if not sys.platform.startswith("darwin"):
+ return None
+
+ if not psutil:
+ return None
+
+ for proc in psutil.process_iter():
+ if proc.name != "Finder":
+ continue
+
+ if proc.username != getpass.getuser():
+ continue
+
+ # Try to isolate system finder as opposed to other "Finder"
+ # processes.
+ if not proc.exe.endswith("CoreServices/Finder.app/Contents/MacOS/Finder"):
+ continue
+
+ return proc.get_cpu_times()
+
+ return None
+
+ def have_high_finder_usage(self):
+ """Determine whether there was high Finder CPU usage during the build.
+
+ Returns True if there was high Finder CPU usage, False if there wasn't,
+ or None if there is nothing to report.
+ """
+ if not self._finder_start_cpu:
+ return None, None
+
+ # We only measure if the measured range is sufficiently long.
+ if self.elapsed < 15:
+ return None, None
+
+ if not self._finder_end_cpu:
+ return None, None
+
+ start = self._finder_start_cpu
+ end = self._finder_end_cpu
+
+ start_total = start.user + start.system
+ end_total = end.user + end.system
+
+ cpu_seconds = end_total - start_total
+
+ # If Finder used more than 25% of 1 core during the build, report an
+ # error.
+ finder_percent = cpu_seconds / self.elapsed * 100
+
+ return finder_percent > 25, finder_percent
+
+ def have_excessive_swapping(self):
+ """Determine whether there was excessive swapping during the build.
+
+ Returns a tuple of (excessive, swap_in, swap_out). All values are None
+ if no swap information is available.
+ """
+ if not self.have_resource_usage:
+ return None, None, None
+
+ swap_in = sum(m.swap.sin for m in self.resources.measurements)
+ swap_out = sum(m.swap.sout for m in self.resources.measurements)
+
+ # The threshold of 1024 MB has been arbitrarily chosen.
+ #
+ # Choosing a proper value that is ideal for everyone is hard. We will
+ # likely iterate on the logic until people are generally satisfied.
+ # If a value is too low, the eventual warning produced does not carry
+ # much meaning. If the threshold is too high, people may not see the
+ # warning and the warning will thus be ineffective.
+ excessive = swap_in > 512 * 1048576 or swap_out > 512 * 1048576
+ return excessive, swap_in, swap_out
+
+ @property
+ def have_resource_usage(self):
+ """Whether resource usage is available."""
+ return self.resources.start_time is not None
+
+ def get_resource_usage(self):
+ """Produce a data structure containing the low-level resource usage information.
+
+ This data structure can e.g. be serialized into JSON and saved for
+ subsequent analysis.
+
+ If no resource usage is available, None is returned.
+ """
+ if not self.have_resource_usage:
+ return None
+
+ cpu_percent = self.resources.aggregate_cpu_percent(phase=None, per_cpu=False)
+ cpu_times = self.resources.aggregate_cpu_times(phase=None, per_cpu=False)
+ io = self.resources.aggregate_io(phase=None)
+
+ o = dict(
+ version=3,
+ argv=sys.argv,
+ start=self.start_time,
+ end=self.end_time,
+ duration=self.end_time - self.start_time,
+ resources=[],
+ cpu_percent=cpu_percent,
+ cpu_times=cpu_times,
+ io=io,
+ objects=self.build_objects,
+ )
+
+ o["tiers"] = self.tiers.tiered_resource_usage()
+
+ self.tiers.add_resource_fields_to_dict(o)
+
+ for usage in self.resources.range_usage():
+ cpu_percent = self.resources.aggregate_cpu_percent(
+ usage.start, usage.end, per_cpu=False
+ )
+ cpu_times = self.resources.aggregate_cpu_times(
+ usage.start, usage.end, per_cpu=False
+ )
+
+ entry = dict(
+ start=usage.start,
+ end=usage.end,
+ virt=list(usage.virt),
+ swap=list(usage.swap),
+ )
+
+ self.tiers.add_resources_to_dict(entry, start=usage.start, end=usage.end)
+
+ o["resources"].append(entry)
+
+ # If the imports for this file ran before the in-tree virtualenv
+ # was bootstrapped (for instance, for a clobber build in automation),
+ # psutil might not be available.
+ #
+ # Treat psutil as optional to avoid an outright failure to log resources
+ # TODO: it would be nice to collect data on the storage device as well
+ # in this case.
+ o["system"] = {}
+ if psutil:
+ o["system"].update(
+ dict(
+ logical_cpu_count=psutil.cpu_count(),
+ physical_cpu_count=psutil.cpu_count(logical=False),
+ swap_total=psutil.swap_memory()[0],
+ vmem_total=psutil.virtual_memory()[0],
+ )
+ )
+
+ return o
+
+ def log_resource_usage(self, usage):
+ """Summarize the resource usage of this build in a log message."""
+
+ if not usage:
+ return
+
+ params = dict(
+ duration=self.end_time - self.start_time,
+ cpu_percent=usage["cpu_percent"],
+ io_read_bytes=usage["io"].read_bytes,
+ io_write_bytes=usage["io"].write_bytes,
+ io_read_time=usage["io"].read_time,
+ io_write_time=usage["io"].write_time,
+ )
+
+ message = (
+ "Overall system resources - Wall time: {duration:.0f}s; "
+ "CPU: {cpu_percent:.0f}%; "
+ "Read bytes: {io_read_bytes}; Write bytes: {io_write_bytes}; "
+ "Read time: {io_read_time}; Write time: {io_write_time}"
+ )
+
+ self.log(logging.WARNING, "resource_usage", params, message)
+
+ excessive, sin, sout = self.have_excessive_swapping()
+ if excessive is not None and (sin or sout):
+ sin /= 1048576
+ sout /= 1048576
+ self.log(
+ logging.WARNING,
+ "swap_activity",
+ {"sin": sin, "sout": sout},
+ "Swap in/out (MB): {sin}/{sout}",
+ )
+
+ def ccache_stats(self, ccache=None):
+ ccache_stats = None
+
+ if ccache is None:
+ ccache = mozfile.which("ccache")
+ if ccache:
+ # With CCache v3.7+ we can use --print-stats
+ has_machine_format = CCacheStats.check_version_3_7_or_newer(ccache)
+ try:
+ output = subprocess.check_output(
+ [ccache, "--print-stats" if has_machine_format else "-s"],
+ universal_newlines=True,
+ )
+ ccache_stats = CCacheStats(output, has_machine_format)
+ except ValueError as e:
+ self.log(logging.WARNING, "ccache", {"msg": str(e)}, "{msg}")
+ return ccache_stats
+
+
+class TerminalLoggingHandler(logging.Handler):
+ """Custom logging handler that works with terminal window dressing.
+
+ This class should probably live elsewhere, like the mach core. Consider
+ this a proving ground for its usefulness.
+ """
+
+ def __init__(self):
+ logging.Handler.__init__(self)
+
+ self.fh = sys.stdout
+ self.footer = None
+
+ def flush(self):
+ self.acquire()
+
+ try:
+ self.fh.flush()
+ finally:
+ self.release()
+
+ def emit(self, record):
+ msg = self.format(record)
+
+ self.acquire()
+
+ try:
+ if self.footer:
+ self.footer.clear()
+
+ self.fh.write(msg)
+ self.fh.write("\n")
+
+ if self.footer:
+ self.footer.draw()
+
+ # If we don't flush, the footer may not get drawn.
+ self.fh.flush()
+ finally:
+ self.release()
+
+
+class BuildProgressFooter(Footer):
+ """Handles display of a build progress indicator in a terminal.
+
+ When mach builds inside a blessed-supported terminal, it will render
+ progress information collected from a BuildMonitor. This class converts the
+ state of BuildMonitor into terminal output.
+ """
+
+ def __init__(self, terminal, monitor):
+ Footer.__init__(self, terminal)
+ self.tiers = six.viewitems(monitor.tiers.tier_status)
+
+ def draw(self):
+ """Draws this footer in the terminal."""
+
+ if not self.tiers:
+ return
+
+ # The drawn terminal looks something like:
+ # TIER: static export libs tools
+
+ parts = [("bold", "TIER:")]
+ append = parts.append
+ for tier, status in self.tiers:
+ if status is None:
+ append(tier)
+ elif status == "finished":
+ append(("green", tier))
+ else:
+ append(("underline_yellow", tier))
+
+ self.write(parts)
+
+
+class OutputManager(LoggingMixin):
+ """Handles writing job output to a terminal or log."""
+
+ def __init__(self, log_manager, footer):
+ self.populate_logger()
+
+ self.footer = None
+ terminal = log_manager.terminal
+
+ # TODO convert terminal footer to config file setting.
+ if not terminal:
+ return
+ if os.environ.get("INSIDE_EMACS", None):
+ return
+
+ if os.environ.get("MACH_NO_TERMINAL_FOOTER", None):
+ footer = None
+
+ self.t = terminal
+ self.footer = footer
+
+ self._handler = TerminalLoggingHandler()
+ self._handler.setFormatter(log_manager.terminal_formatter)
+ self._handler.footer = self.footer
+
+ old = log_manager.replace_terminal_handler(self._handler)
+ self._handler.level = old.level
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if self.footer:
+ self.footer.clear()
+ # Prevents the footer from being redrawn if logging occurs.
+ self._handler.footer = None
+
+ def write_line(self, line):
+ if self.footer:
+ self.footer.clear()
+
+ print(line)
+
+ if self.footer:
+ self.footer.draw()
+
+ def refresh(self):
+ if not self.footer:
+ return
+
+ self.footer.clear()
+ self.footer.draw()
+
+
+class BuildOutputManager(OutputManager):
+ """Handles writing build output to a terminal, to logs, etc."""
+
+ def __init__(self, log_manager, monitor, footer):
+ self.monitor = monitor
+ OutputManager.__init__(self, log_manager, footer)
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ OutputManager.__exit__(self, exc_type, exc_value, traceback)
+
+ # Ensure the resource monitor is stopped because leaving it running
+ # could result in the process hanging on exit because the resource
+ # collection child process hasn't been told to stop.
+ self.monitor.stop_resource_recording()
+
+ def on_line(self, line):
+ warning, state_changed, message = self.monitor.on_line(line)
+
+ if message:
+ self.log(logging.INFO, "build_output", {"line": message}, "{line}")
+ elif state_changed:
+ have_handler = hasattr(self, "handler")
+ if have_handler:
+ self.handler.acquire()
+ try:
+ self.refresh()
+ finally:
+ if have_handler:
+ self.handler.release()
+
+
+class StaticAnalysisFooter(Footer):
+ """Handles display of a static analysis progress indicator in a terminal."""
+
+ def __init__(self, terminal, monitor):
+ Footer.__init__(self, terminal)
+ self.monitor = monitor
+
+ def draw(self):
+ """Draws this footer in the terminal."""
+
+ monitor = self.monitor
+ total = monitor.num_files
+ processed = monitor.num_files_processed
+ percent = "(%.2f%%)" % (processed * 100.0 / total)
+ parts = [
+ ("bright_black", "Processing"),
+ ("yellow", str(processed)),
+ ("bright_black", "of"),
+ ("yellow", str(total)),
+ ("bright_black", "files"),
+ ("green", percent),
+ ]
+ if monitor.current_file:
+ parts.append(("bold", monitor.current_file))
+
+ self.write(parts)
+
+
+class StaticAnalysisOutputManager(OutputManager):
+ """Handles writing static analysis output to a terminal or file."""
+
+ def __init__(self, log_manager, monitor, footer):
+ self.monitor = monitor
+ self.raw = ""
+ OutputManager.__init__(self, log_manager, footer)
+
+ def on_line(self, line):
+ warning, relevant = self.monitor.on_line(line)
+ if relevant:
+ self.raw += line + "\n"
+
+ if warning:
+ self.log(
+ logging.INFO,
+ "compiler_warning",
+ warning,
+ "Warning: {flag} in {filename}: {message}",
+ )
+
+ if relevant:
+ self.log(logging.INFO, "build_output", {"line": line}, "{line}")
+ else:
+ have_handler = hasattr(self, "handler")
+ if have_handler:
+ self.handler.acquire()
+ try:
+ self.refresh()
+ finally:
+ if have_handler:
+ self.handler.release()
+
+ def write(self, path, output_format):
+ assert output_format in ("text", "json"), "Invalid output format {}".format(
+ output_format
+ )
+ path = os.path.realpath(path)
+
+ if output_format == "json":
+ self.monitor._warnings_database.save_to_file(path)
+
+ else:
+ with io.open(path, "w", encoding="utf-8", newline="\n") as f:
+ f.write(self.raw)
+
+ self.log(
+ logging.INFO,
+ "write_output",
+ {"path": path, "format": output_format},
+ "Wrote {format} output in {path}",
+ )
+
+
+class CCacheStats(object):
+ """Holds statistics from ccache.
+
+ Instances can be subtracted from each other to obtain differences.
+ print() or str() the object to show a ``ccache -s`` like output
+ of the captured stats.
+
+ """
+
+ STATS_KEYS = [
+ # (key, description)
+ # Refer to stats.c in ccache project for all the descriptions.
+ ("stats_zeroed", ("stats zeroed", "stats zero time")),
+ ("stats_updated", "stats updated"),
+ ("cache_hit_direct", "cache hit (direct)"),
+ ("cache_hit_preprocessed", "cache hit (preprocessed)"),
+ ("cache_hit_rate", "cache hit rate"),
+ ("cache_miss", "cache miss"),
+ ("link", "called for link"),
+ ("preprocessing", "called for preprocessing"),
+ ("multiple", "multiple source files"),
+ ("stdout", "compiler produced stdout"),
+ ("no_output", "compiler produced no output"),
+ ("empty_output", "compiler produced empty output"),
+ ("failed", "compile failed"),
+ ("error", "ccache internal error"),
+ ("preprocessor_error", "preprocessor error"),
+ ("cant_use_pch", "can't use precompiled header"),
+ ("compiler_missing", "couldn't find the compiler"),
+ ("cache_file_missing", "cache file missing"),
+ ("bad_args", "bad compiler arguments"),
+ ("unsupported_lang", "unsupported source language"),
+ ("compiler_check_failed", "compiler check failed"),
+ ("autoconf", "autoconf compile/link"),
+ ("unsupported_code_directive", "unsupported code directive"),
+ ("unsupported_compiler_option", "unsupported compiler option"),
+ ("out_stdout", "output to stdout"),
+ ("out_device", "output to a non-regular file"),
+ ("no_input", "no input file"),
+ ("bad_extra_file", "error hashing extra file"),
+ ("num_cleanups", "cleanups performed"),
+ ("cache_files", "files in cache"),
+ ("cache_size", "cache size"),
+ ("cache_max_size", "max cache size"),
+ ]
+
+ SKIP_LINES = (
+ "cache directory",
+ "primary config",
+ "secondary config",
+ )
+
+ STATS_KEYS_3_7_PLUS = {
+ "stats_zeroed_timestamp": "stats_zeroed",
+ "stats_updated_timestamp": "stats_updated",
+ "direct_cache_hit": "cache_hit_direct",
+ "preprocessed_cache_hit": "cache_hit_preprocessed",
+ # "cache_hit_rate" is not provided
+ "cache_miss": "cache_miss",
+ "called_for_link": "link",
+ "called_for_preprocessing": "preprocessing",
+ "multiple_source_files": "multiple",
+ "compiler_produced_stdout": "stdout",
+ "compiler_produced_no_output": "no_output",
+ "compiler_produced_empty_output": "empty_output",
+ "compile_failed": "failed",
+ "internal_error": "error",
+ "preprocessor_error": "preprocessor_error",
+ "could_not_use_precompiled_header": "cant_use_pch",
+ "could_not_find_compiler": "compiler_missing",
+ "missing_cache_file": "cache_file_missing",
+ "bad_compiler_arguments": "bad_args",
+ "unsupported_source_language": "unsupported_lang",
+ "compiler_check_failed": "compiler_check_failed",
+ "autoconf_test": "autoconf",
+ "unsupported_code_directive": "unsupported_code_directive",
+ "unsupported_compiler_option": "unsupported_compiler_option",
+ "output_to_stdout": "out_stdout",
+ "output_to_a_non_file": "out_device",
+ "no_input_file": "no_input",
+ "error_hashing_extra_file": "bad_extra_file",
+ "cleanups_performed": "num_cleanups",
+ "files_in_cache": "cache_files",
+ "cache_size_kibibyte": "cache_size",
+ # "cache_max_size" is obsolete and not printed anymore
+ }
+
+ ABSOLUTE_KEYS = {"cache_files", "cache_size", "cache_max_size"}
+ FORMAT_KEYS = {"cache_size", "cache_max_size"}
+
+ GiB = 1024 ** 3
+ MiB = 1024 ** 2
+ KiB = 1024
+
+ def __init__(self, output=None, has_machine_format=False):
+ """Construct an instance from the output of ccache -s."""
+ self._values = {}
+
+ if not output:
+ return
+
+ if has_machine_format:
+ self._parse_machine_format(output)
+ else:
+ self._parse_human_format(output)
+
+ def _parse_machine_format(self, output):
+ for line in output.splitlines():
+ line = line.strip()
+ key, _, value = line.partition("\t")
+ stat_key = self.STATS_KEYS_3_7_PLUS.get(key)
+ if stat_key:
+ value = int(value)
+ if key.endswith("_kibibyte"):
+ value *= 1024
+ self._values[stat_key] = value
+
+ (direct, preprocessed, miss) = self.hit_rates()
+ self._values["cache_hit_rate"] = (direct + preprocessed) * 100
+
+ def _parse_human_format(self, output):
+ for line in output.splitlines():
+ line = line.strip()
+ if line:
+ self._parse_line(line)
+
+ def _parse_line(self, line):
+ line = six.ensure_text(line)
+ for stat_key, stat_description in self.STATS_KEYS:
+ if line.startswith(stat_description):
+ raw_value = self._strip_prefix(line, stat_description)
+ self._values[stat_key] = self._parse_value(raw_value)
+ break
+ else:
+ if not line.startswith(self.SKIP_LINES):
+ raise ValueError("Failed to parse ccache stats output: %s" % line)
+
+ @staticmethod
+ def _strip_prefix(line, prefix):
+ if isinstance(prefix, tuple):
+ for p in prefix:
+ line = CCacheStats._strip_prefix(line, p)
+ return line
+ return line[len(prefix) :].strip() if line.startswith(prefix) else line
+
+ @staticmethod
+ def _parse_value(raw_value):
+ try:
+ # ccache calls strftime with '%c' (src/stats.c)
+ ts = time.strptime(raw_value, "%c")
+ return int(time.mktime(ts))
+ except ValueError:
+ if raw_value == "never":
+ return 0
+ pass
+
+ value = raw_value.split()
+ unit = ""
+ if len(value) == 1:
+ numeric = value[0]
+ elif len(value) == 2:
+ numeric, unit = value
+ else:
+ raise ValueError("Failed to parse ccache stats value: %s" % raw_value)
+
+ if "." in numeric:
+ numeric = float(numeric)
+ else:
+ numeric = int(numeric)
+
+ if unit in ("GB", "Gbytes"):
+ unit = CCacheStats.GiB
+ elif unit in ("MB", "Mbytes"):
+ unit = CCacheStats.MiB
+ elif unit in ("KB", "Kbytes"):
+ unit = CCacheStats.KiB
+ else:
+ unit = 1
+
+ return int(numeric * unit)
+
+ def hit_rate_message(self):
+ return (
+ "ccache (direct) hit rate: {:.1%}; (preprocessed) hit rate: {:.1%};"
+ " miss rate: {:.1%}".format(*self.hit_rates())
+ )
+
+ def hit_rates(self):
+ direct = self._values["cache_hit_direct"]
+ preprocessed = self._values["cache_hit_preprocessed"]
+ miss = self._values["cache_miss"]
+ total = float(direct + preprocessed + miss)
+
+ if total > 0:
+ direct /= total
+ preprocessed /= total
+ miss /= total
+
+ return (direct, preprocessed, miss)
+
+ def __sub__(self, other):
+ result = CCacheStats()
+
+ for k, prefix in self.STATS_KEYS:
+ if k not in self._values and k not in other._values:
+ continue
+
+ our_value = self._values.get(k, 0)
+ other_value = other._values.get(k, 0)
+
+ if k in self.ABSOLUTE_KEYS:
+ result._values[k] = our_value
+ else:
+ result._values[k] = our_value - other_value
+
+ return result
+
+ def __str__(self):
+ LEFT_ALIGN = 34
+ lines = []
+
+ for stat_key, stat_description in self.STATS_KEYS:
+ if stat_key not in self._values:
+ continue
+
+ value = self._values[stat_key]
+
+ if stat_key in self.FORMAT_KEYS:
+ value = "%15s" % self._format_value(value)
+ else:
+ value = "%8u" % value
+
+ if isinstance(stat_description, tuple):
+ stat_description = stat_description[0]
+
+ lines.append("%s%s" % (stat_description.ljust(LEFT_ALIGN), value))
+
+ return "\n".join(lines)
+
+ def __nonzero__(self):
+ relative_values = [
+ v for k, v in self._values.items() if k not in self.ABSOLUTE_KEYS
+ ]
+ return all(v >= 0 for v in relative_values) and any(
+ v > 0 for v in relative_values
+ )
+
+ def __bool__(self):
+ return self.__nonzero__()
+
+ @staticmethod
+ def _format_value(v):
+ if v > CCacheStats.GiB:
+ return "%.1f Gbytes" % (float(v) / CCacheStats.GiB)
+ elif v > CCacheStats.MiB:
+ return "%.1f Mbytes" % (float(v) / CCacheStats.MiB)
+ else:
+ return "%.1f Kbytes" % (float(v) / CCacheStats.KiB)
+
+ @staticmethod
+ def check_version_3_7_or_newer(ccache):
+ output_version = subprocess.check_output(
+ [ccache, "--version"], universal_newlines=True
+ )
+ return CCacheStats._is_version_3_7_or_newer(output_version)
+
+ @staticmethod
+ def _is_version_3_7_or_newer(output):
+ if "ccache version" not in output:
+ return False
+
+ major = 0
+ minor = 0
+
+ for line in output.splitlines():
+ version = re.search(r"ccache version (\d+).(\d+).*", line)
+ if version:
+ major = int(version.group(1))
+ minor = int(version.group(2))
+ break
+
+ return ((major << 8) + minor) >= ((3 << 8) + 7)
+
+
+class BuildDriver(MozbuildObject):
+ """Provides a high-level API for build actions."""
+
+ def __init__(self, *args, **kwargs):
+ MozbuildObject.__init__(self, *args, virtualenv_name="build", **kwargs)
+ self.metrics = None
+ self.mach_context = None
+
+ def build(
+ self,
+ metrics,
+ what=None,
+ jobs=0,
+ job_size=0,
+ directory=None,
+ verbose=False,
+ keep_going=False,
+ mach_context=None,
+ append_env=None,
+ virtualenv_topobjdir=None,
+ ):
+ """Invoke the build backend.
+
+ ``what`` defines the thing to build. If not defined, the default
+ target is used.
+ """
+ self.metrics = metrics
+ self.mach_context = mach_context
+ warnings_path = self._get_state_filename("warnings.json")
+ monitor = self._spawn(BuildMonitor)
+ monitor.init(warnings_path)
+ footer = BuildProgressFooter(self.log_manager.terminal, monitor)
+
+ # Disable indexing in objdir because it is not necessary and can slow
+ # down builds.
+ mkdir(self.topobjdir, not_indexed=True)
+
+ with BuildOutputManager(self.log_manager, monitor, footer) as output:
+ monitor.start()
+
+ if directory is not None and not what:
+ print("Can only use -C/--directory with an explicit target " "name.")
+ return 1
+
+ if directory is not None:
+ directory = mozpath.normsep(directory)
+ if directory.startswith("/"):
+ directory = directory[1:]
+
+ monitor.start_resource_recording()
+
+ if self._check_clobber(self.mozconfig, os.environ):
+ return 1
+
+ self.mach_context.command_attrs["clobber"] = False
+ self.metrics.mozbuild.clobber.set(False)
+ config = None
+ try:
+ config = self.config_environment
+ except Exception:
+ # If we don't already have a config environment this is either
+ # a fresh objdir or $OBJDIR/config.status has been removed for
+ # some reason, which indicates a clobber of sorts.
+ self.mach_context.command_attrs["clobber"] = True
+ self.metrics.mozbuild.clobber.set(True)
+
+ # Record whether a clobber was requested so we can print
+ # a special message later if the build fails.
+ clobber_requested = False
+
+ # Write out any changes to the current mozconfig in case
+ # they should invalidate configure.
+ self._write_mozconfig_json()
+
+ previous_backend = None
+ if config is not None:
+ previous_backend = config.substs.get("BUILD_BACKENDS", [None])[0]
+
+ config_rc = None
+ # Even if we have a config object, it may be out of date
+ # if something that influences its result has changed.
+ if config is None or self.build_out_of_date(
+ mozpath.join(self.topobjdir, "config.status"),
+ mozpath.join(self.topobjdir, "config_status_deps.in"),
+ ):
+ if previous_backend and "Make" not in previous_backend:
+ clobber_requested = self._clobber_configure()
+
+ if config is None:
+ print(" Config object not found by mach.")
+
+ config_rc = self.configure(
+ metrics,
+ buildstatus_messages=True,
+ line_handler=output.on_line,
+ append_env=append_env,
+ virtualenv_topobjdir=virtualenv_topobjdir,
+ )
+
+ if config_rc != 0:
+ return config_rc
+
+ config = self.reload_config_environment()
+
+ if config.substs.get("MOZ_USING_CCACHE"):
+ ccache = config.substs.get("CCACHE")
+ ccache_start = monitor.ccache_stats(ccache)
+ else:
+ ccache_start = None
+
+ # Collect glean metrics
+ substs = config.substs
+ mozbuild_metrics = metrics.mozbuild
+ mozbuild_metrics.compiler.set(substs.get("CC_TYPE", None))
+
+ def get_substs_flag(name):
+ return bool(substs.get(name, None))
+
+ mozbuild_metrics.artifact.set(get_substs_flag("MOZ_ARTIFACT_BUILDS"))
+ mozbuild_metrics.debug.set(get_substs_flag("MOZ_DEBUG"))
+ mozbuild_metrics.opt.set(get_substs_flag("MOZ_OPTIMIZE"))
+ mozbuild_metrics.ccache.set(get_substs_flag("CCACHE"))
+ using_sccache = get_substs_flag("MOZ_USING_SCCACHE")
+ mozbuild_metrics.sccache.set(using_sccache)
+ mozbuild_metrics.icecream.set(get_substs_flag("CXX_IS_ICECREAM"))
+ mozbuild_metrics.project.set(substs.get("MOZ_BUILD_APP", ""))
+
+ all_backends = config.substs.get("BUILD_BACKENDS", [None])
+ active_backend = all_backends[0]
+
+ status = None
+
+ if not config_rc and any(
+ [
+ self.backend_out_of_date(
+ mozpath.join(self.topobjdir, "backend.%sBackend" % backend)
+ )
+ for backend in all_backends
+ ]
+ ):
+ print("Build configuration changed. Regenerating backend.")
+ args = [
+ config.substs["PYTHON3"],
+ mozpath.join(self.topobjdir, "config.status"),
+ ]
+ self.run_process(args, cwd=self.topobjdir, pass_thru=True)
+
+ if jobs == 0:
+ for param in self.mozconfig.get("make_extra") or []:
+ key, value = param.split("=", 1)
+ if key == "MOZ_PARALLEL_BUILD":
+ jobs = int(value)
+
+ if "Make" not in active_backend:
+ backend_cls = get_backend_class(active_backend)(config)
+ status = backend_cls.build(self, output, jobs, verbose, what)
+
+ if status and clobber_requested:
+ for line in CLOBBER_REQUESTED_MESSAGE.splitlines():
+ self.log(
+ logging.WARNING, "clobber", {"msg": line.rstrip()}, "{msg}"
+ )
+
+ if what and status is None:
+ # Collect target pairs.
+ target_pairs = []
+ for target in what:
+ path_arg = self._wrap_path_argument(target)
+
+ if directory is not None:
+ make_dir = os.path.join(self.topobjdir, directory)
+ make_target = target
+ else:
+ make_dir, make_target = resolve_target_to_make(
+ self.topobjdir, path_arg.relpath()
+ )
+
+ if make_dir is None and make_target is None:
+ return 1
+
+ if config.is_artifact_build and target.startswith("installers-"):
+ # See https://bugzilla.mozilla.org/show_bug.cgi?id=1387485
+ print(
+ "Localized Builds are not supported with Artifact Builds enabled.\n"
+ "You should disable Artifact Builds (Use --disable-compile-environment "
+ "in your mozconfig instead) then re-build to proceed."
+ )
+ return 1
+
+ # See bug 886162 - we don't want to "accidentally" build
+ # the entire tree (if that's really the intent, it's
+ # unlikely they would have specified a directory.)
+ if not make_dir and not make_target:
+ print(
+ "The specified directory doesn't contain a "
+ "Makefile and the first parent with one is the "
+ "root of the tree. Please specify a directory "
+ "with a Makefile or run |mach build| if you "
+ "want to build the entire tree."
+ )
+ return 1
+
+ target_pairs.append((make_dir, make_target))
+
+ # Build target pairs.
+ for make_dir, make_target in target_pairs:
+ # We don't display build status messages during partial
+ # tree builds because they aren't reliable there. This
+ # could potentially be fixed if the build monitor were more
+ # intelligent about encountering undefined state.
+ no_build_status = "1" if make_dir is not None else ""
+ tgt_env = dict(append_env or {})
+ tgt_env["NO_BUILDSTATUS_MESSAGES"] = no_build_status
+ status = self._run_make(
+ directory=make_dir,
+ target=make_target,
+ line_handler=output.on_line,
+ log=False,
+ print_directory=False,
+ ensure_exit_code=False,
+ num_jobs=jobs,
+ job_size=job_size,
+ silent=not verbose,
+ append_env=tgt_env,
+ keep_going=keep_going,
+ )
+
+ if status != 0:
+ break
+
+ elif status is None:
+ # If the backend doesn't specify a build() method, then just
+ # call client.mk directly.
+ status = self._run_client_mk(
+ line_handler=output.on_line,
+ jobs=jobs,
+ job_size=job_size,
+ verbose=verbose,
+ keep_going=keep_going,
+ append_env=append_env,
+ )
+
+ self.log(
+ logging.WARNING,
+ "warning_summary",
+ {"count": len(monitor.warnings_database)},
+ "{count} compiler warnings present.",
+ )
+
+ # Try to run the active build backend's post-build step, if possible.
+ try:
+ active_backend = config.substs.get("BUILD_BACKENDS", [None])[0]
+ if active_backend:
+ backend_cls = get_backend_class(active_backend)(config)
+ new_status = backend_cls.post_build(
+ self, output, jobs, verbose, status
+ )
+ status = new_status
+ except Exception as ex:
+ self.log(
+ logging.DEBUG,
+ "post_build",
+ {"ex": str(ex)},
+ "Unable to run active build backend's post-build step; "
+ + "failing the build due to exception: {ex}.",
+ )
+ if not status:
+ # If the underlying build provided a failing status, pass
+ # it through; otherwise, fail.
+ status = 1
+
+ record_usage = status == 0
+
+ # On automation, only record usage for plain `mach build`
+ if "MOZ_AUTOMATION" in os.environ and what:
+ record_usage = False
+
+ monitor.finish(record_usage=record_usage)
+
+ if status == 0:
+ usage = monitor.get_resource_usage()
+ if usage:
+ self.mach_context.command_attrs["usage"] = usage
+
+ # Print the collected compiler warnings. This is redundant with
+ # inline output from the compiler itself. However, unlike inline
+ # output, this list is sorted and grouped by file, making it
+ # easier to triage output.
+ #
+ # Only do this if we had a successful build. If the build failed,
+ # there are more important things in the log to look for than
+ # whatever code we warned about.
+ if not status:
+ # Suppress warnings for 3rd party projects in local builds
+ # until we suppress them for real.
+ # TODO remove entries/feature once we stop generating warnings
+ # in these directories.
+ pathToThirdparty = os.path.join(
+ self.topsrcdir, "tools", "rewriting", "ThirdPartyPaths.txt"
+ )
+
+ pathToGenerated = os.path.join(
+ self.topsrcdir, "tools", "rewriting", "Generated.txt"
+ )
+
+ if os.path.exists(pathToThirdparty):
+ with io.open(
+ pathToThirdparty, encoding="utf-8", newline="\n"
+ ) as f, io.open(pathToGenerated, encoding="utf-8", newline="\n") as g:
+ # Normalize the path (no trailing /)
+ LOCAL_SUPPRESS_DIRS = tuple(
+ [line.strip("\n/") for line in f]
+ + [line.strip("\n/") for line in g]
+ )
+ else:
+ # For application based on gecko like thunderbird
+ LOCAL_SUPPRESS_DIRS = ()
+
+ suppressed_by_dir = Counter()
+
+ THIRD_PARTY_CODE = "third-party code"
+ suppressed = set(
+ w.replace("-Wno-error=", "-W")
+ for w in substs.get("WARNINGS_CFLAGS", [])
+ + substs.get("WARNINGS_CXXFLAGS", [])
+ if w.startswith("-Wno-error=")
+ )
+ warnings = []
+ for warning in sorted(monitor.instance_warnings):
+ path = mozpath.normsep(warning["filename"])
+ if path.startswith(self.topsrcdir):
+ path = path[len(self.topsrcdir) + 1 :]
+
+ warning["normpath"] = path
+
+ if "MOZ_AUTOMATION" not in os.environ:
+ if path.startswith(LOCAL_SUPPRESS_DIRS):
+ suppressed_by_dir[THIRD_PARTY_CODE] += 1
+ continue
+
+ if warning["flag"] in suppressed:
+ suppressed_by_dir[os.path.dirname(path)] += 1
+ continue
+
+ warnings.append(warning)
+
+ if THIRD_PARTY_CODE in suppressed_by_dir:
+ suppressed_third_party_code = [
+ (THIRD_PARTY_CODE, suppressed_by_dir.pop(THIRD_PARTY_CODE))
+ ]
+ else:
+ suppressed_third_party_code = []
+ for d, count in suppressed_third_party_code + sorted(
+ suppressed_by_dir.items()
+ ):
+ self.log(
+ logging.WARNING,
+ "suppressed_warning",
+ {"dir": d, "count": count},
+ "(suppressed {count} warnings in {dir})",
+ )
+
+ for warning in warnings:
+ if warning["column"] is not None:
+ self.log(
+ logging.WARNING,
+ "compiler_warning",
+ warning,
+ "warning: {normpath}:{line}:{column} [{flag}] " "{message}",
+ )
+ else:
+ self.log(
+ logging.WARNING,
+ "compiler_warning",
+ warning,
+ "warning: {normpath}:{line} [{flag}] {message}",
+ )
+
+ high_finder, finder_percent = monitor.have_high_finder_usage()
+ if high_finder:
+ print(FINDER_SLOW_MESSAGE % finder_percent)
+
+ if config.substs.get("MOZ_USING_CCACHE"):
+ ccache_end = monitor.ccache_stats(ccache)
+ else:
+ ccache_end = None
+
+ ccache_diff = None
+ if ccache_start and ccache_end:
+ ccache_diff = ccache_end - ccache_start
+ if ccache_diff:
+ self.log(
+ logging.INFO,
+ "ccache",
+ {"msg": ccache_diff.hit_rate_message()},
+ "{msg}",
+ )
+
+ notify_minimum_time = 300
+ try:
+ notify_minimum_time = int(os.environ.get("MACH_NOTIFY_MINTIME", "300"))
+ except ValueError:
+ # Just stick with the default
+ pass
+
+ if monitor.elapsed > notify_minimum_time:
+ # Display a notification when the build completes.
+ self.notify("Build complete" if not status else "Build failed")
+
+ if status:
+ if what and any(
+ [target for target in what if target not in ("faster", "binaries")]
+ ):
+ print(
+ "Hey! Builds initiated with `mach build "
+ "$A_SPECIFIC_TARGET` may not always work, even if the "
+ "code being built is correct. Consider doing a bare "
+ "`mach build` instead."
+ )
+ return status
+
+ if monitor.have_resource_usage:
+ excessive, swap_in, swap_out = monitor.have_excessive_swapping()
+ # if excessive:
+ # print(EXCESSIVE_SWAP_MESSAGE)
+
+ print("To view resource usage of the build, run |mach " "resource-usage|.")
+
+ long_build = monitor.elapsed > 1200
+
+ if long_build:
+ output.on_line(
+ "We know it took a while, but your build finally finished successfully!"
+ )
+ if not using_sccache:
+ output.on_line(
+ "If you are building Firefox often, SCCache can save you a lot "
+ "of time. You can learn more here: "
+ "https://firefox-source-docs.mozilla.org/setup/"
+ "configuring_build_options.html#sccache"
+ )
+ else:
+ output.on_line("Your build was successful!")
+
+ # Only for full builds because incremental builders likely don't
+ # need to be burdened with this.
+ if not what:
+ try:
+ # Fennec doesn't have useful output from just building. We should
+ # arguably make the build action useful for Fennec. Another day...
+ if self.substs["MOZ_BUILD_APP"] != "mobile/android":
+ print("To take your build for a test drive, run: |mach run|")
+ app = self.substs["MOZ_BUILD_APP"]
+ if app in ("browser", "mobile/android"):
+ print(
+ "For more information on what to do now, see "
+ "https://firefox-source-docs.mozilla.org/setup/contributing_code.html" # noqa
+ )
+ except Exception:
+ # Ignore Exceptions in case we can't find config.status (such
+ # as when doing OSX Universal builds)
+ pass
+
+ return status
+
+ def configure(
+ self,
+ metrics,
+ options=None,
+ buildstatus_messages=False,
+ line_handler=None,
+ append_env=None,
+ virtualenv_topobjdir=None,
+ ):
+ # Disable indexing in objdir because it is not necessary and can slow
+ # down builds.
+ self.metrics = metrics
+ mkdir(self.topobjdir, not_indexed=True)
+ self._write_mozconfig_json()
+
+ def on_line(line):
+ self.log(logging.INFO, "build_output", {"line": line}, "{line}")
+
+ line_handler = line_handler or on_line
+
+ append_env = dict(append_env or {})
+
+ # Back when client.mk was used, `mk_add_options "export ..."` lines
+ # from the mozconfig would spill into the configure environment, so
+ # add that for backwards compatibility.
+ for line in self.mozconfig["make_extra"] or []:
+ if line.startswith("export "):
+ k, eq, v = line[len("export ") :].partition("=")
+ if eq == "=":
+ append_env[k] = v
+
+ virtualenv_topobjdir = virtualenv_topobjdir or self.topobjdir
+ build_site = CommandSiteManager.from_environment(
+ self.topsrcdir,
+ lambda: get_state_dir(specific_to_topsrcdir=True, topsrcdir=self.topsrcdir),
+ "build",
+ os.path.join(virtualenv_topobjdir, "_virtualenvs"),
+ )
+ build_site.ensure()
+
+ command = [build_site.python_path, os.path.join(self.topsrcdir, "configure.py")]
+ if options:
+ command.extend(options)
+
+ if buildstatus_messages:
+ line_handler("BUILDSTATUS TIERS configure")
+ line_handler("BUILDSTATUS TIER_START configure")
+
+ env = os.environ.copy()
+ env.update(append_env)
+
+ with subprocess.Popen(
+ command,
+ cwd=self.topobjdir,
+ env=env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ universal_newlines=True,
+ ) as process:
+ for line in process.stdout:
+ line_handler(line.rstrip())
+ status = process.wait()
+ if buildstatus_messages:
+ line_handler("BUILDSTATUS TIER_FINISH configure")
+ if status:
+ print('*** Fix above errors and then restart with "./mach build"')
+ else:
+ print("Configure complete!")
+ print("Be sure to run |mach build| to pick up any changes")
+
+ return status
+
+ def install_tests(self):
+ """Install test files."""
+
+ if self.is_clobber_needed():
+ print(
+ INSTALL_TESTS_CLOBBER.format(
+ clobber_file=os.path.join(self.topobjdir, "CLOBBER")
+ )
+ )
+ sys.exit(1)
+
+ install_test_files(mozpath.normpath(self.topsrcdir), self.topobjdir, "_tests")
+
+ def _clobber_configure(self):
+ # This is an optimistic treatment of the CLOBBER file for when we have
+ # some trust in the build system: an update to the CLOBBER file is
+ # interpreted to mean that configure will fail during an incremental
+ # build, which is handled by removing intermediate configure artifacts
+ # and subsections of the objdir related to python and testing before
+ # proceeding.
+ clobberer = Clobberer(self.topsrcdir, self.topobjdir)
+ clobber_output = io.StringIO()
+ res = clobberer.maybe_do_clobber(os.getcwd(), False, clobber_output)
+ required, performed, message = res
+ assert not performed
+ if not required:
+ return False
+
+ def remove_objdir_path(path):
+ path = mozpath.join(self.topobjdir, path)
+ self.log(
+ logging.WARNING,
+ "clobber",
+ {"path": path},
+ "CLOBBER file has been updated, removing {path}.",
+ )
+ mozfile.remove(path)
+
+ # Remove files we think could cause "configure" clobber bugs.
+ for f in ("old-configure.vars", "config.cache", "configure.pkl"):
+ remove_objdir_path(f)
+ remove_objdir_path(mozpath.join("js", "src", f))
+
+ rm_dirs = [
+ # Stale paths in our virtualenv may cause build-backend
+ # to fail.
+ "_virtualenvs",
+ # Some tests may accumulate state in the objdir that may
+ # become invalid after srcdir changes.
+ "_tests",
+ ]
+
+ for d in rm_dirs:
+ remove_objdir_path(d)
+
+ os.utime(mozpath.join(self.topobjdir, "CLOBBER"), None)
+ return True
+
+ def _write_mozconfig_json(self):
+ mozconfig_json = os.path.join(self.topobjdir, ".mozconfig.json")
+ with FileAvoidWrite(mozconfig_json) as fh:
+ to_write = six.ensure_text(
+ json.dumps(
+ {
+ "topsrcdir": self.topsrcdir,
+ "topobjdir": self.topobjdir,
+ "mozconfig": self.mozconfig,
+ },
+ sort_keys=True,
+ indent=2,
+ )
+ )
+ # json.dumps in python2 inserts some trailing whitespace while
+ # json.dumps in python3 does not, which defeats the FileAvoidWrite
+ # mechanism. Strip the trailing whitespace to avoid rewriting this
+ # file unnecessarily.
+ to_write = "\n".join([line.rstrip() for line in to_write.splitlines()])
+ fh.write(to_write)
+
+ def _run_client_mk(
+ self,
+ target=None,
+ line_handler=None,
+ jobs=0,
+ job_size=0,
+ verbose=None,
+ keep_going=False,
+ append_env=None,
+ ):
+ append_env = dict(append_env or {})
+ append_env["TOPSRCDIR"] = self.topsrcdir
+
+ append_env["CONFIG_GUESS"] = self.resolve_config_guess()
+
+ mozconfig = self.mozconfig
+
+ mozconfig_make_lines = []
+ for arg in mozconfig["make_extra"] or []:
+ mozconfig_make_lines.append(arg)
+
+ if mozconfig["make_flags"]:
+ mozconfig_make_lines.append(
+ "MOZ_MAKE_FLAGS=%s" % " ".join(mozconfig["make_flags"])
+ )
+ objdir = mozpath.normsep(self.topobjdir)
+ mozconfig_make_lines.append("MOZ_OBJDIR=%s" % objdir)
+ mozconfig_make_lines.append("OBJDIR=%s" % objdir)
+
+ if mozconfig["path"]:
+ mozconfig_make_lines.append(
+ "FOUND_MOZCONFIG=%s" % mozpath.normsep(mozconfig["path"])
+ )
+ mozconfig_make_lines.append("export FOUND_MOZCONFIG")
+
+ # The .mozconfig.mk file only contains exported variables and lines with
+ # UPLOAD_EXTRA_FILES.
+ mozconfig_filtered_lines = [
+ line
+ for line in mozconfig_make_lines
+ # Bug 1418122 investigate why UPLOAD_EXTRA_FILES is special and
+ # remove it.
+ if line.startswith("export ") or "UPLOAD_EXTRA_FILES" in line
+ ]
+
+ mozconfig_client_mk = os.path.join(self.topobjdir, ".mozconfig-client-mk")
+ with FileAvoidWrite(mozconfig_client_mk) as fh:
+ fh.write("\n".join(mozconfig_make_lines))
+
+ mozconfig_mk = os.path.join(self.topobjdir, ".mozconfig.mk")
+ with FileAvoidWrite(mozconfig_mk) as fh:
+ fh.write("\n".join(mozconfig_filtered_lines))
+
+ # Copy the original mozconfig to the objdir.
+ mozconfig_objdir = os.path.join(self.topobjdir, ".mozconfig")
+ if mozconfig["path"]:
+ with open(mozconfig["path"], "r") as ifh:
+ with FileAvoidWrite(mozconfig_objdir) as ofh:
+ ofh.write(ifh.read())
+ else:
+ try:
+ os.unlink(mozconfig_objdir)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+
+ if mozconfig_make_lines:
+ self.log(
+ logging.WARNING,
+ "mozconfig_content",
+ {
+ "path": mozconfig["path"],
+ "content": "\n ".join(mozconfig_make_lines),
+ },
+ "Adding make options from {path}\n {content}",
+ )
+
+ append_env["OBJDIR"] = mozpath.normsep(self.topobjdir)
+
+ return self._run_make(
+ srcdir=True,
+ filename="client.mk",
+ ensure_exit_code=False,
+ print_directory=False,
+ target=target,
+ line_handler=line_handler,
+ log=False,
+ num_jobs=jobs,
+ job_size=job_size,
+ silent=not verbose,
+ keep_going=keep_going,
+ append_env=append_env,
+ )
+
+ def _check_clobber(self, mozconfig, env):
+ """Run `Clobberer.maybe_do_clobber`, log the result and return a status bool.
+
+ Wraps the clobbering logic in `Clobberer.maybe_do_clobber` to provide logging
+ and handling of the `AUTOCLOBBER` mozconfig option.
+
+ Return a bool indicating whether the clobber reached an error state. For example,
+ return `True` if the clobber was required but not completed, and return `False` if
+ the clobber was not required and not completed.
+ """
+ auto_clobber = any(
+ [
+ env.get("AUTOCLOBBER", False),
+ (mozconfig["env"] or {}).get("added", {}).get("AUTOCLOBBER", False),
+ "AUTOCLOBBER=1" in (mozconfig["make_extra"] or []),
+ ]
+ )
+ from mozbuild.base import BuildEnvironmentNotFoundException
+
+ substs = dict()
+ try:
+ substs = self.substs
+ except BuildEnvironmentNotFoundException:
+ # We'll just use an empty substs if there is no config.
+ pass
+ clobberer = Clobberer(self.topsrcdir, self.topobjdir, substs)
+ clobber_output = six.StringIO()
+ res = clobberer.maybe_do_clobber(os.getcwd(), auto_clobber, clobber_output)
+ clobber_output.seek(0)
+ for line in clobber_output.readlines():
+ self.log(logging.WARNING, "clobber", {"msg": line.rstrip()}, "{msg}")
+
+ clobber_required, clobber_performed, clobber_message = res
+ if clobber_required and not clobber_performed:
+ for line in clobber_message.splitlines():
+ self.log(logging.WARNING, "clobber", {"msg": line.rstrip()}, "{msg}")
+ return True
+
+ if clobber_performed and env.get("TINDERBOX_OUTPUT"):
+ self.log(
+ logging.WARNING,
+ "clobber",
+ {"msg": "TinderboxPrint: auto clobber"},
+ "{msg}",
+ )
+
+ return False