Bug 1412460 - Move code for invoking build system out of mach_commands.py; r?build draft
authorGregory Szorc <gps@mozilla.com>
Fri, 27 Oct 2017 10:58:30 -0700
changeset 690323 0f213bfc138252a45eb476b6b4da91f4d525349e
parent 690322 4dc600a838c29b1a2939c3ef6e82549833d9c1eb
child 690324 66f402e9bd107784d4260b8f69c305b032a0ee7f
child 690344 3b99501daa2f6eb7c3f687a0d655727053058f7b
push id87282
push usergszorc@mozilla.com
push dateWed, 01 Nov 2017 21:48:55 +0000
reviewersbuild
bugs1412460
milestone58.0a1
Bug 1412460 - Move code for invoking build system out of mach_commands.py; r?build The goal is to move this lower-level code somewhere where it can be reused. The only meaningful code change as part of the move was handling of the mach context. Before, we could access self._mach_context. After, we need to pass it as an argument. This also uncovered a bug where "telemetry_handler" could be None but we called it unconditionally. We now guard this call with an "if." MozReview-Commit-ID: FWw4l6H1ayS
python/mozbuild/mozbuild/controller/building.py
python/mozbuild/mozbuild/mach_commands.py
--- a/python/mozbuild/mozbuild/controller/building.py
+++ b/python/mozbuild/mozbuild/controller/building.py
@@ -9,40 +9,68 @@ import json
 import logging
 import os
 import subprocess
 import sys
 import time
 import which
 
 from collections import (
+    Counter,
     namedtuple,
     OrderedDict,
 )
+from textwrap import (
+    TextWrapper,
+)
 
 try:
     import psutil
 except Exception:
     psutil = None
 
 from mach.mixin.logging import LoggingMixin
 from mozsystemmonitor.resourcemonitor import SystemResourceMonitor
 
 import mozpack.path as mozpath
 
-from ..base import MozbuildObject
-
-from ..testing import install_test_files
-
+from ..base import (
+    BuildEnvironmentNotFoundException,
+    MozbuildObject,
+)
+from ..backend import (
+    get_backend_class,
+)
+from ..testing import (
+    install_test_files,
+)
 from ..compilation.warnings import (
     WarningsCollector,
     WarningsDatabase,
 )
+from ..util import (
+    mkdir,
+    resolve_target_to_make,
+)
 
-from textwrap import TextWrapper
+
+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:
@@ -942,16 +970,315 @@ class CCacheStats(object):
             return '%.1f Mbytes' % (float(v) / CCacheStats.MiB)
         else:
             return '%.1f Kbytes' % (float(v) / CCacheStats.KiB)
 
 
 class BuildDriver(MozbuildObject):
     """Provides a high-level API for build actions."""
 
+    def build(self, what=None, disable_extra_make_dependencies=None, jobs=0,
+              directory=None, verbose=False, keep_going=False, mach_context=None):
+        """Invoke the build backend.
+
+        ``what`` defines the thing to build. If not defined, the default
+        target is used.
+        """
+        self.log_manager.register_structured_logger(logging.getLogger('mozbuild'))
+
+        warnings_path = self._get_state_filename('warnings.json')
+        monitor = self._spawn(BuildMonitor)
+        monitor.init(warnings_path)
+        ccache_start = monitor.ccache_stats()
+        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:
+                disable_extra_make_dependencies=True
+                directory = mozpath.normsep(directory)
+                if directory.startswith('/'):
+                    directory = directory[1:]
+
+            status = None
+            monitor.start_resource_recording()
+            if what:
+                top_make = os.path.join(self.topobjdir, 'Makefile')
+                if not os.path.exists(top_make):
+                    print('Your tree has not been configured yet. Please run '
+                        '|mach build| with no arguments.')
+                    return 1
+
+                # 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
+
+                    # 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))
+
+                # Possibly add extra make depencies using dumbmake.
+                if not disable_extra_make_dependencies:
+                    from dumbmake.dumbmake import (dependency_map,
+                                                   add_extra_dependencies)
+                    depfile = os.path.join(self.topsrcdir, 'build',
+                                           'dumbmake-dependencies')
+                    with open(depfile) as f:
+                        dm = dependency_map(f.readlines())
+                    new_pairs = list(add_extra_dependencies(target_pairs, dm))
+                    self.log(logging.DEBUG, 'dumbmake',
+                             {'target_pairs': target_pairs,
+                              'new_pairs': new_pairs},
+                             'Added extra dependencies: will build {new_pairs} ' +
+                             'instead of {target_pairs}.')
+                    target_pairs = new_pairs
+
+                # Ensure build backend is up to date. The alternative is to
+                # have rules in the invoked Makefile to rebuild the build
+                # backend. But that involves make reinvoking itself and there
+                # are undesired side-effects of this. See bug 877308 for a
+                # comprehensive history lesson.
+                self._run_make(directory=self.topobjdir, target='backend',
+                    line_handler=output.on_line, log=False,
+                    print_directory=False, keep_going=keep_going)
+
+                # 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.
+                    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, silent=not verbose,
+                        append_env={b'NO_BUILDSTATUS_MESSAGES': b'1'},
+                        keep_going=keep_going)
+
+                    if status != 0:
+                        break
+            else:
+                # Try to call the default backend's build() method. This will
+                # run configure to determine BUILD_BACKENDS if it hasn't run
+                # yet.
+                config = None
+                try:
+                    config = self.config_environment
+                except Exception:
+                    config_rc = self.configure(buildstatus_messages=True,
+                                               line_handler=output.on_line)
+                    if config_rc != 0:
+                        return config_rc
+
+                    # Even if configure runs successfully, we may have trouble
+                    # getting the config_environment for some builds, such as
+                    # OSX Universal builds. These have to go through client.mk
+                    # regardless.
+                    try:
+                        config = self.config_environment
+                    except Exception:
+                        pass
+
+                if config:
+                    active_backend = config.substs.get('BUILD_BACKENDS', [None])[0]
+                    if active_backend:
+                        backend_cls = get_backend_class(active_backend)(config)
+                        status = backend_cls.build(self, output, jobs, verbose)
+
+                # If the backend doesn't specify a build() method, then just
+                # call client.mk directly.
+                if status is None:
+                    status = self._run_make(srcdir=True, filename='client.mk',
+                        line_handler=output.on_line, log=False, print_directory=False,
+                        allow_parallel=False, ensure_exit_code=False, num_jobs=jobs,
+                        silent=not verbose, keep_going=keep_going)
+
+                self.log(logging.WARNING, 'warning_summary',
+                    {'count': len(monitor.warnings_database)},
+                    '{count} compiler warnings present.')
+
+            # 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")
+
+                if os.path.exists(pathToThirdparty):
+                    with open(pathToThirdparty) as f:
+                        # Normalize the path (no trailing /)
+                        LOCAL_SUPPRESS_DIRS = tuple(d.rstrip('/') for d in f.read().splitlines())
+                else:
+                    # For application based on gecko like thunderbird
+                    LOCAL_SUPPRESS_DIRS = ()
+
+                suppressed_by_dir = Counter()
+
+                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 (path.startswith(LOCAL_SUPPRESS_DIRS) and
+                            'MOZ_AUTOMATION' not in os.environ):
+                        for d in LOCAL_SUPPRESS_DIRS:
+                            if path.startswith(d):
+                                suppressed_by_dir[d] += 1
+                                break
+
+                        continue
+
+                    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}')
+
+                for d, count in sorted(suppressed_by_dir.items()):
+                    self.log(logging.WARNING, 'suppressed_warning',
+                             {'dir': d, 'count': count},
+                             '(suppressed {count} warnings in {dir})')
+
+            monitor.finish(record_usage=status==0)
+
+        high_finder, finder_percent = monitor.have_high_finder_usage()
+        if high_finder:
+            print(FINDER_SLOW_MESSAGE % finder_percent)
+
+        ccache_end = monitor.ccache_stats()
+
+        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:
+            return status
+
+        long_build = monitor.elapsed > 600
+
+        if long_build:
+            output.on_line('We know it took a while, but your build finally finished successfully!')
+        else:
+            output.on_line('Your build was successful!')
+
+        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|.')
+
+            telemetry_handler = getattr(mach_context,
+                                        'telemetry_handler', None)
+            telemetry_data = monitor.get_resource_usage()
+
+            # Record build configuration data. For now, we cherry pick
+            # items we need rather than grabbing everything, in order
+            # to avoid accidentally disclosing PII.
+            telemetry_data['substs'] = {}
+            try:
+                for key in ['MOZ_ARTIFACT_BUILDS', 'MOZ_USING_CCACHE', 'MOZ_USING_SCCACHE']:
+                    value = self.substs.get(key, False)
+                    telemetry_data['substs'][key] = value
+            except BuildEnvironmentNotFoundException:
+                pass
+
+            # Grab ccache stats if available. We need to be careful not
+            # to capture information that can potentially identify the
+            # user (such as the cache location)
+            if ccache_diff:
+                telemetry_data['ccache'] = {}
+                for key in [key[0] for key in ccache_diff.STATS_KEYS]:
+                    try:
+                        telemetry_data['ccache'][key] = ccache_diff._values[key]
+                    except KeyError:
+                        pass
+
+            if telemetry_handler:
+                telemetry_handler(mach_context, telemetry_data)
+
+        # 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://developer.mozilla.org/docs/Developer_Guide/So_You_Just_Built_Firefox')
+            except Exception:
+                # Ignore Exceptions in case we can't find config.status (such
+                # as when doing OSX Universal builds)
+                pass
+
+        return status
+
     def install_tests(self, test_objs):
         """Install test files."""
 
         if self.is_clobber_needed():
             print(INSTALL_TESTS_CLOBBER.format(
                   clobber_file=os.path.join(self.topobjdir, 'CLOBBER')))
             sys.exit(1)
 
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -26,51 +26,36 @@ from mach.decorators import (
     Command,
     SettingsProvider,
     SubCommand,
 )
 
 from mach.main import Mach
 
 from mozbuild.base import (
-    BuildEnvironmentNotFoundException,
     MachCommandBase,
     MachCommandConditions as conditions,
     MozbuildObject,
 )
 from mozbuild.util import ensureParentDir
 
 from mozbuild.backend import (
     backends,
-    get_backend_class,
 )
 from mozbuild.shellutil import quote as shell_quote
 
 
 BUILD_WHAT_HELP = '''
 What to build. Can be a top-level make target or a relative directory. If
 multiple options are provided, they will be built serially. Takes dependency
 information from `topsrcdir/build/dumbmake-dependencies` to build additional
 targets as needed. BUILDING ONLY PARTS OF THE TREE CAN RESULT IN BAD TREE
 STATE. USE AT YOUR OWN RISK.
 '''.strip()
 
-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()
 
 EXCESSIVE_SWAP_MESSAGE = '''
 ===================
 PERFORMANCE WARNING
 
 Your machine experienced a lot of swap activity during the build. This is
 possibly a sign that your machine doesn't have enough physical memory or
 not enough available memory to perform the build. It's also possible some
@@ -166,315 +151,28 @@ class Build(MachCommandBase):
 
         * faster - builds JavaScript, XUL, CSS, etc files.
 
         "binaries" and "faster" almost fully complement each other. However,
         there are build actions not captured by either. If things don't appear to
         be rebuilding, perform a vanilla `mach build` to rebuild the world.
         """
         from mozbuild.controller.building import (
-            BuildMonitor,
-            BuildOutputManager,
-            BuildProgressFooter,
-        )
-        from mozbuild.util import (
-            mkdir,
-            resolve_target_to_make,
+            BuildDriver,
         )
 
-        self.log_manager.register_structured_logger(logging.getLogger('mozbuild'))
-
-        warnings_path = self._get_state_filename('warnings.json')
-        monitor = self._spawn(BuildMonitor)
-        monitor.init(warnings_path)
-        ccache_start = monitor.ccache_stats()
-        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:
-                disable_extra_make_dependencies=True
-                directory = mozpath.normsep(directory)
-                if directory.startswith('/'):
-                    directory = directory[1:]
-
-            status = None
-            monitor.start_resource_recording()
-            if what:
-                top_make = os.path.join(self.topobjdir, 'Makefile')
-                if not os.path.exists(top_make):
-                    print('Your tree has not been configured yet. Please run '
-                        '|mach build| with no arguments.')
-                    return 1
-
-                # 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
-
-                    # 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))
-
-                # Possibly add extra make depencies using dumbmake.
-                if not disable_extra_make_dependencies:
-                    from dumbmake.dumbmake import (dependency_map,
-                                                   add_extra_dependencies)
-                    depfile = os.path.join(self.topsrcdir, 'build',
-                                           'dumbmake-dependencies')
-                    with open(depfile) as f:
-                        dm = dependency_map(f.readlines())
-                    new_pairs = list(add_extra_dependencies(target_pairs, dm))
-                    self.log(logging.DEBUG, 'dumbmake',
-                             {'target_pairs': target_pairs,
-                              'new_pairs': new_pairs},
-                             'Added extra dependencies: will build {new_pairs} ' +
-                             'instead of {target_pairs}.')
-                    target_pairs = new_pairs
-
-                # Ensure build backend is up to date. The alternative is to
-                # have rules in the invoked Makefile to rebuild the build
-                # backend. But that involves make reinvoking itself and there
-                # are undesired side-effects of this. See bug 877308 for a
-                # comprehensive history lesson.
-                self._run_make(directory=self.topobjdir, target='backend',
-                    line_handler=output.on_line, log=False,
-                    print_directory=False, keep_going=keep_going)
-
-                # 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.
-                    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, silent=not verbose,
-                        append_env={b'NO_BUILDSTATUS_MESSAGES': b'1'},
-                        keep_going=keep_going)
-
-                    if status != 0:
-                        break
-            else:
-                # Try to call the default backend's build() method. This will
-                # run configure to determine BUILD_BACKENDS if it hasn't run
-                # yet.
-                config = None
-                try:
-                    config = self.config_environment
-                except Exception:
-                    config_rc = self.configure(buildstatus_messages=True,
-                                               line_handler=output.on_line)
-                    if config_rc != 0:
-                        return config_rc
-
-                    # Even if configure runs successfully, we may have trouble
-                    # getting the config_environment for some builds, such as
-                    # OSX Universal builds. These have to go through client.mk
-                    # regardless.
-                    try:
-                        config = self.config_environment
-                    except Exception:
-                        pass
-
-                if config:
-                    active_backend = config.substs.get('BUILD_BACKENDS', [None])[0]
-                    if active_backend:
-                        backend_cls = get_backend_class(active_backend)(config)
-                        status = backend_cls.build(self, output, jobs, verbose)
-
-                # If the backend doesn't specify a build() method, then just
-                # call client.mk directly.
-                if status is None:
-                    status = self._run_make(srcdir=True, filename='client.mk',
-                        line_handler=output.on_line, log=False, print_directory=False,
-                        allow_parallel=False, ensure_exit_code=False, num_jobs=jobs,
-                        silent=not verbose, keep_going=keep_going)
-
-                self.log(logging.WARNING, 'warning_summary',
-                    {'count': len(monitor.warnings_database)},
-                    '{count} compiler warnings present.')
-
-            # 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")
-
-                if os.path.exists(pathToThirdparty):
-                    with open(pathToThirdparty) as f:
-                        # Normalize the path (no trailing /)
-                        LOCAL_SUPPRESS_DIRS = tuple(d.rstrip('/') for d in f.read().splitlines())
-                else:
-                    # For application based on gecko like thunderbird
-                    LOCAL_SUPPRESS_DIRS = ()
-
-                suppressed_by_dir = collections.Counter()
-
-                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 (path.startswith(LOCAL_SUPPRESS_DIRS) and
-                            'MOZ_AUTOMATION' not in os.environ):
-                        for d in LOCAL_SUPPRESS_DIRS:
-                            if path.startswith(d):
-                                suppressed_by_dir[d] += 1
-                                break
-
-                        continue
-
-                    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}')
-
-                for d, count in sorted(suppressed_by_dir.items()):
-                    self.log(logging.WARNING, 'suppressed_warning',
-                             {'dir': d, 'count': count},
-                             '(suppressed {count} warnings in {dir})')
-
-            monitor.finish(record_usage=status==0)
-
-        high_finder, finder_percent = monitor.have_high_finder_usage()
-        if high_finder:
-            print(FINDER_SLOW_MESSAGE % finder_percent)
-
-        ccache_end = monitor.ccache_stats()
-
-        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:
-            return status
-
-        long_build = monitor.elapsed > 600
-
-        if long_build:
-            output.on_line('We know it took a while, but your build finally finished successfully!')
-        else:
-            output.on_line('Your build was successful!')
-
-        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|.')
-
-            telemetry_handler = getattr(self._mach_context,
-                                        'telemetry_handler', None)
-            telemetry_data = monitor.get_resource_usage()
-
-            # Record build configuration data. For now, we cherry pick
-            # items we need rather than grabbing everything, in order
-            # to avoid accidentally disclosing PII.
-            telemetry_data['substs'] = {}
-            try:
-                for key in ['MOZ_ARTIFACT_BUILDS', 'MOZ_USING_CCACHE', 'MOZ_USING_SCCACHE']:
-                    value = self.substs.get(key, False)
-                    telemetry_data['substs'][key] = value
-            except BuildEnvironmentNotFoundException:
-                pass
-
-            # Grab ccache stats if available. We need to be careful not
-            # to capture information that can potentially identify the
-            # user (such as the cache location)
-            if ccache_diff:
-                telemetry_data['ccache'] = {}
-                for key in [key[0] for key in ccache_diff.STATS_KEYS]:
-                    try:
-                        telemetry_data['ccache'][key] = ccache_diff._values[key]
-                    except KeyError:
-                        pass
-
-            telemetry_handler(self._mach_context, telemetry_data)
-
-        # 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://developer.mozilla.org/docs/Developer_Guide/So_You_Just_Built_Firefox')
-            except Exception:
-                # Ignore Exceptions in case we can't find config.status (such
-                # as when doing OSX Universal builds)
-                pass
-
-        return status
+        driver = self._spawn(BuildDriver)
+        return driver.build(
+            what=what,
+            disable_extra_make_dependencies=disable_extra_make_dependencies,
+            jobs=jobs,
+            directory=directory,
+            verbose=verbose,
+            keep_going=keep_going,
+            mach_context=self._mach_context)
 
     @Command('configure', category='build',
         description='Configure the tree (run configure and config.status).')
     @CommandArgument('options', default=None, nargs=argparse.REMAINDER,
                      help='Configure options')
     def configure(self, options=None, buildstatus_messages=False, line_handler=None):
         def on_line(line):
             self.log(logging.INFO, 'build_output', {'line': line}, '{line}')