Backed out 2 changesets (bug 1027890) for B2G Windows Build bustage on a CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Tue, 24 Jun 2014 18:33:46 -0700
changeset 190647 59b0072d64aa5bebe2906b1778727d8172ac0f84
parent 190646 71f6b8a9b2e8cb698a495eb29d7611cb030073a9
child 190648 a6d4659e10318523f43e6eb5beac93141e81d109
push id8358
push usercbook@mozilla.com
push dateWed, 25 Jun 2014 14:22:36 +0000
treeherderb2g-inbound@56ebaa70ca72 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1027890
milestone33.0a1
backs outbcd694f0e95dad42f9d355c2f8c8a99715b714ad
49cadfcde709e64b3f31476fb1370ee8d8ca1403
Backed out 2 changesets (bug 1027890) for B2G Windows Build bustage on a CLOSED TREE Backed out changeset bcd694f0e95d (bug 1027890) Backed out changeset 49cadfcde709 (bug 1027890)
Makefile.in
b2g/gaia/Makefile.in
build/autoconf/compiler-opts.m4
build/docs/slow.rst
build/pymake/make.py
client.mk
config/baseconfig.mk
config/config.mk
config/makefiles/functions.mk
config/rules.mk
config/tests/makefiles/autodeps/Makefile.in
intl/icu/Makefile.in
js/src/Makefile.in
python/mozbuild/mozbuild/base.py
python/mozbuild/mozbuild/mach_commands.py
security/build/Makefile.in
--- a/Makefile.in
+++ b/Makefile.in
@@ -1,20 +1,22 @@
 #
 # 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/.
 
+ifndef .PYMAKE
 ifeq (,$(MAKE_VERSION))
 $(error GNU Make is required)
 endif
 make_min_ver := 3.81
 ifneq ($(make_min_ver),$(firstword $(sort $(make_min_ver) $(MAKE_VERSION))))
 $(error GNU Make $(make_min_ver) or higher is required)
 endif
+endif
 
 export TOPLEVEL_BUILD := 1
 
 default::
 
 ifdef MOZ_BUILD_APP
 include $(topsrcdir)/$(MOZ_BUILD_APP)/build.mk
 endif
--- a/b2g/gaia/Makefile.in
+++ b/b2g/gaia/Makefile.in
@@ -1,17 +1,24 @@
 # 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/.
 
 GAIA_PATH := gaia/profile
 
+ifdef .PYMAKE
+# For use of GNU make in pymake builds.
+GAIA_MAKE=$(GMAKE)
+else
+GAIA_MAKE=$(MAKE)
+endif
+
 # This is needed to avoid making run-b2g depend on mozglue
 WRAP_LDFLAGS :=
 
 GENERATED_DIRS += $(DIST)/bin/$(GAIA_PATH)
 
 include $(topsrcdir)/config/rules.mk
 
 libs::
-	+$(MAKE) -j1 -C $(GAIADIR) clean
-	+$(MAKE) -j1 -C $(GAIADIR) profile
+	+$(GAIA_MAKE) -j1 -C $(GAIADIR) clean
+	+$(GAIA_MAKE) -j1 -C $(GAIADIR) profile
 	(cd $(GAIADIR)/profile && tar $(TAR_CREATE_FLAGS) - .) | (cd $(abspath $(DIST))/bin/$(GAIA_PATH) && tar -xf -)
--- a/build/autoconf/compiler-opts.m4
+++ b/build/autoconf/compiler-opts.m4
@@ -179,16 +179,22 @@ if test -n "$MOZ_DEBUG" -o -n "$MOZ_DEBU
     export MOZ_DEBUG_SYMBOLS
 fi
 
 ])
 
 dnl A high level macro for selecting compiler options.
 AC_DEFUN([MOZ_COMPILER_OPTS],
 [
+  if test "${MOZ_PSEUDO_DERECURSE-unset}" = unset; then
+    dnl Don't enable on pymake, because of bug 918652. Bug 912979 is an annoyance
+    dnl with pymake, too.
+    MOZ_PSEUDO_DERECURSE=no-pymake
+  fi
+
   MOZ_DEBUGGING_OPTS
   MOZ_RTTI
 if test "$CLANG_CXX"; then
     ## We disable return-type-c-linkage because jsval is defined as a C++ type but is
     ## returned by C functions. This is possible because we use knowledge about the ABI
     ## to typedef it to a C type with the same layout when the headers are included
     ## from C.
     _WARNINGS_CXXFLAGS="${_WARNINGS_CXXFLAGS} -Wno-unknown-warning-option -Wno-return-type-c-linkage"
--- a/build/docs/slow.rst
+++ b/build/docs/slow.rst
@@ -83,19 +83,32 @@ the build system.
 
 **We recommend building with no less than 8 GB of system memory.** As
 always, the more memory you have, the better. For a bare bones machine
 doing nothing more than building the source tree, anything more than 16
 GB is likely entering the point of diminishing returns.
 
 This cause impacts both clobber and incremental builds.
 
+You are building with pymake
+============================
+
+Pymake is slower than GNU make. One reason is Python is generally slower
+than C. The build system maintainers are consistently looking at
+optimizing pymake. However, it is death by a thousand cuts.
+
+This cause impacts both clobber and incremental builds.
+
 You are building on Windows
 ===========================
 
+Builds on Windows are slow for a few reasons. First, Windows builds use
+pymake, not GNU make (because of compatibility issues with GNU make).
+But, there are other sources of slowness.
+
 New processes on Windows are about a magnitude slower to spawn than on
 UNIX-y systems such as Linux. This is because Windows has optimized new
 threads while the \*NIX platforms typically optimize new processes.
 Anyway, the build system spawns thousands of new processes during a
 build. Parts of the build that rely on rapid spawning of new processes
 are slow on Windows as a result. This is most pronounced when running
 *configure*. The configure file is a giant shell script and shell
 scripts rely heavily on new processes. This is why configure on Windows
--- a/build/pymake/make.py
+++ b/build/pymake/make.py
@@ -13,22 +13,23 @@ import gc
 
 if __name__ == '__main__':
   if 'TINDERBOX_OUTPUT' in os.environ:
     # When building on mozilla build slaves, execute mozmake instead. Until bug
     # 978211, this is the easiest, albeit hackish, way to do this.
     import subprocess
     mozmake = os.path.join(os.path.dirname(__file__), '..', '..',
         'mozmake.exe')
-    cmd = [mozmake]
-    cmd.extend(sys.argv[1:])
-    shell = os.environ.get('SHELL')
-    if shell and not shell.lower().endswith('.exe'):
-        cmd += ['SHELL=%s.exe' % shell]
-    sys.exit(subprocess.call(cmd))
+    if os.path.exists(mozmake):
+        cmd = [mozmake]
+        cmd.extend(sys.argv[1:])
+        shell = os.environ.get('SHELL')
+        if shell and not shell.lower().endswith('.exe'):
+            cmd += ['SHELL=%s.exe' % shell]
+        sys.exit(subprocess.call(cmd))
 
   sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
   sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', 0)
 
   gc.disable()
 
   pymake.command.main(sys.argv[1:], os.environ, os.getcwd(), cb=sys.exit)
   pymake.process.ParallelContext.spin()
--- a/client.mk
+++ b/client.mk
@@ -139,16 +139,23 @@ endif
 ifeq (,$(findstring -j,$(MOZ_MAKE_FLAGS)))
   cores=$(shell $(PYTHON) -c 'import multiprocessing; print(multiprocessing.cpu_count())')
   MOZ_MAKE_FLAGS += -j$(cores)
 endif
 
 
 ifndef MOZ_OBJDIR
   MOZ_OBJDIR = obj-$(CONFIG_GUESS)
+else
+# On Windows Pymake builds check MOZ_OBJDIR doesn't start with "/"
+  ifneq (,$(findstring mingw,$(CONFIG_GUESS)))
+  ifeq (1_a,$(.PYMAKE)_$(firstword a$(subst /, ,$(MOZ_OBJDIR))))
+  $(error For Windows Pymake builds, MOZ_OBJDIR must be a Windows [and not MSYS] style path.)
+  endif
+  endif
 endif
 
 ifdef MOZ_BUILD_PROJECTS
 
 ifdef MOZ_CURRENT_PROJECT
   OBJDIR = $(MOZ_OBJDIR)/$(MOZ_CURRENT_PROJECT)
   MOZ_MAKE = $(MAKE) $(MOZ_MAKE_FLAGS) -C $(OBJDIR)
   BUILD_PROJECT_ARG = MOZ_BUILD_APP=$(MOZ_CURRENT_PROJECT)
--- a/config/baseconfig.mk
+++ b/config/baseconfig.mk
@@ -20,36 +20,37 @@ endif
 endif
 
 # We do magic with OBJ_SUFFIX in config.mk, the following ensures we don't
 # manually use it before config.mk inclusion
 _OBJ_SUFFIX := $(OBJ_SUFFIX)
 OBJ_SUFFIX = $(error config/config.mk needs to be included before using OBJ_SUFFIX)
 
 ifeq ($(HOST_OS_ARCH),WINNT)
-# We only support building with a non-msys gnu make version
+# We only support building with pymake or a non-msys gnu make version
 # strictly above 4.0.
-ifdef .PYMAKE
-$(error Pymake is no longer supported. Please upgrade to MozillaBuild 1.9 or newer and build with 'mach' or 'mozmake')
-endif
-
+ifndef .PYMAKE
 ifeq (a,$(firstword a$(subst /, ,$(abspath .))))
 $(error MSYS make is not supported)
 endif
 # 4.0- happens to be greater than 4.0, lower than the mozmake version,
 # and lower than 4.0.1 or 4.1, whatever next version of gnu make will
 # be released.
 ifneq (4.0-,$(firstword $(sort 4.0- $(MAKE_VERSION))))
 $(error Make version too old. Only versions strictly greater than 4.0 are supported.)
 endif
-
+endif
 ifdef INCLUDED_AUTOCONF_MK
 ifeq (a,$(firstword a$(subst /, ,$(srcdir))))
 $(error MSYS-style srcdir are not supported for Windows builds.)
 endif
 endif
 endif # WINNT
 
+ifdef .PYMAKE
+include_deps = $(eval $(if $(2),,-)includedeps $(1))
+else
 include_deps = $(eval $(if $(2),,-)include $(1))
+endif
 
 ifndef INCLUDED_AUTOCONF_MK
 default::
 endif
--- a/config/config.mk
+++ b/config/config.mk
@@ -189,16 +189,26 @@ BUILD_TOOLS	= $(WIN_TOP_SRC)/build/unix
 else
 win_srcdir	:= $(srcdir)
 BUILD_TOOLS	= $(topsrcdir)/build/unix
 endif
 
 CONFIG_TOOLS	= $(MOZ_BUILD_ROOT)/config
 AUTOCONF_TOOLS	= $(topsrcdir)/build/autoconf
 
+# Disable MOZ_PSEUDO_DERECURSE when it contains no-pymake and we're running
+# pymake. This can be removed when no-pymake is removed from the default in
+# build/autoconf/compiler-opts.m4.
+ifdef .PYMAKE
+comma = ,
+ifneq (,$(filter no-pymake,$(subst $(comma), ,$(MOZ_PSEUDO_DERECURSE))))
+MOZ_PSEUDO_DERECURSE :=
+endif
+endif
+
 # Disable MOZ_PSEUDO_DERECURSE on PGO builds until it's fixed.
 ifneq (,$(MOZ_PROFILE_USE)$(MOZ_PROFILE_GENERATE))
 MOZ_PSEUDO_DERECURSE :=
 endif
 
 #
 # Strip off the excessively long version numbers on these platforms,
 # but save the version to allow multiple versions of the same base
@@ -223,16 +233,20 @@ CXX_WRAPPER ?= $(call py_action,cl)
 endif # _MSC_VER
 
 CC := $(CC_WRAPPER) $(CC)
 CXX := $(CXX_WRAPPER) $(CXX)
 MKDIR ?= mkdir
 SLEEP ?= sleep
 TOUCH ?= touch
 
+ifdef .PYMAKE
+PYCOMMANDPATH += $(PYTHON_SITE_PACKAGES)
+endif
+
 PYTHON_PATH = $(PYTHON) $(topsrcdir)/config/pythonpath.py
 
 # determine debug-related options
 _DEBUG_ASFLAGS :=
 _DEBUG_CFLAGS :=
 _DEBUG_LDFLAGS :=
 
 ifdef MOZ_DEBUG
@@ -701,16 +715,19 @@ NSINSTALL = $(NSINSTALL_PY)
 else
 NSINSTALL = $(DIST)/bin/nsinstall$(HOST_BIN_SUFFIX)
 endif # WINNT
 endif # NSINSTALL_BIN
 
 
 ifeq (,$(CROSS_COMPILE)$(filter-out WINNT, $(OS_ARCH)))
 INSTALL = $(NSINSTALL) -t
+ifdef .PYMAKE
+install_cmd = $(NSINSTALL_NATIVECMD) -t $(1)
+endif # .PYMAKE
 
 else
 
 # This isn't laid out as conditional directives so that NSDISTMODE can be
 # target-specific.
 INSTALL         = $(if $(filter copy, $(NSDISTMODE)), $(NSINSTALL) -t, $(if $(filter absolute_symlink, $(NSDISTMODE)), $(NSINSTALL) -L $(PWD), $(NSINSTALL) -R))
 
 endif # WINNT
--- a/config/makefiles/functions.mk
+++ b/config/makefiles/functions.mk
@@ -22,9 +22,13 @@ core_realpath = $(error core_realpath is
 core_winabspath = $(error core_winabspath is unsupported)
 
 # Run a named Python build action. The first argument is the name of the build
 # action. The second argument are the arguments to pass to the action (space
 # delimited arguments). e.g.
 #
 #   libs::
 #       $(call py_action,purge_manifests,_build_manifests/purge/foo.manifest)
+ifdef .PYMAKE
+py_action = %mozbuild.action.$(1) main $(2)
+else
 py_action = $(PYTHON) -m mozbuild.action.$(1) $(2)
+endif
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -30,37 +30,56 @@ endif
 
 ifndef INCLUDED_VERSION_MK
 include $(topsrcdir)/config/version.mk
 endif
 
 USE_AUTOTARGETS_MK = 1
 include $(topsrcdir)/config/makefiles/makeutils.mk
 
+# Only build with Pymake (not GNU make) on Windows.
+ifeq ($(HOST_OS_ARCH),WINNT)
+ifndef L10NBASEDIR
+ifndef .PYMAKE
+$(error Pymake is required to build on Windows. Run |./mach build| to \
+automatically use pymake or invoke pymake directly via \
+|python build/pymake/make.py|.)
+endif
+endif
+endif
+
 ifdef REBUILD_CHECK
+ifdef .PYMAKE
+REPORT_BUILD = @%rebuild_check rebuild_check $@ $^
+else
 REPORT_BUILD = $(info $(shell $(PYTHON) $(MOZILLA_DIR)/config/rebuild_check.py $@ $^))
+endif
 else
 REPORT_BUILD = $(info $(notdir $@))
 endif
 
 EXEC			= exec
 
 # Don't copy xulrunner files at install time, when using system xulrunner
 ifdef SYSTEM_LIBXUL
   SKIP_COPY_XULRUNNER=1
 endif
 
 # ELOG prints out failed command when building silently (gmake -s). Pymake
 # prints out failed commands anyway, so ELOG just makes things worse by
 # forcing shell invocations.
+ifndef .PYMAKE
 ifneq (,$(findstring s, $(filter-out --%, $(MAKEFLAGS))))
   ELOG := $(EXEC) sh $(BUILD_TOOLS)/print-failed-commands.sh
 else
   ELOG :=
 endif # -s
+else
+  ELOG :=
+endif # ifndef .PYMAKE
 
 _VPATH_SRCS = $(abspath $<)
 
 ################################################################################
 # Testing frameworks support
 ################################################################################
 
 testxpcobjdir = $(DEPTH)/_tests/xpcshell
@@ -1118,17 +1137,19 @@ endif
 	@echo Creating Resource file: $@
 ifdef GNU_CC
 	$(RC) $(RCFLAGS) $(filter-out -U%,$(DEFINES)) $(INCLUDES:-I%=--include-dir %) $(OUTOPTION)$@ $(_VPATH_SRCS)
 else
 	$(RC) $(RCFLAGS) -r $(DEFINES) $(INCLUDES) $(OUTOPTION)$@ $(_VPATH_SRCS)
 endif
 
 # Cancel GNU make built-in implicit rules
+ifndef .PYMAKE
 MAKEFLAGS += -r
+endif
 
 ifneq (,$(filter WINNT,$(OS_ARCH)))
 SEP := ;
 else
 SEP := :
 endif
 
 EMPTY :=
--- a/config/tests/makefiles/autodeps/Makefile.in
+++ b/config/tests/makefiles/autodeps/Makefile.in
@@ -17,16 +17,17 @@ include $(topsrcdir)/config/rules.mk
 autotgt_tests = .deps/autotargets.mk.ts
 
 tgts =\
   .deps/.mkdir.done\
   $(autotgt_tests)
   $(NULL)
 
 export MAKE
+export .PYMAKE
 
 ##------------------_##
 ##---]  TARGETS  [---##
 ##------------------_##
 all::
 
 check:: $(tgts)
 
--- a/intl/icu/Makefile.in
+++ b/intl/icu/Makefile.in
@@ -39,29 +39,37 @@ ifdef ENABLE_INTL_API
     endif # MOZ_SHARED_ICU
   endif # !MOZ_NATIVE_ICU
 endif # ENABLE_INTL_API
 
 include $(topsrcdir)/config/rules.mk
 
 ifdef ENABLE_INTL_API
 ifndef MOZ_NATIVE_ICU
+ifdef .PYMAKE
+ICU_MAKE = $(GMAKE)
+else
+ICU_MAKE = $(MAKE)
+endif
+
 default:: buildicu
 
+# - ICU requires GNU make according to its readme.html. pymake can't be used
+#   because it doesn't support order only dependencies.
 # - Force ICU to use the standard suffix for object files because expandlibs
 #   will discard all files with a non-standard suffix (bug 857450).
 # - Options for genrb: -k strict parsing; -R omit collation tailoring rules.
 buildicu::
 # ICU's build system is full of races, so force non-parallel build.
 ifdef CROSS_COMPILE
-	+$(MAKE) -j1 -C host STATIC_O=$(OBJ_SUFFIX) GENRBOPTS='-k -R -C'
+	+$(ICU_MAKE) -j1 -C host STATIC_O=$(OBJ_SUFFIX) GENRBOPTS='-k -R -C'
 endif
-	+$(MAKE) -j1 -C target STATIC_O=$(OBJ_SUFFIX) GENRBOPTS='-k -R'
+	+$(ICU_MAKE) -j1 -C target STATIC_O=$(OBJ_SUFFIX) GENRBOPTS='-k -R'
 	$(ICU_LIB_RENAME)
 
 distclean clean::
 ifdef CROSS_COMPILE
-	+$(MAKE) -C host $@ STATIC_O=$(OBJ_SUFFIX)
+	+$(ICU_MAKE) -C host $@ STATIC_O=$(OBJ_SUFFIX)
 endif
-	+$(MAKE) -C target $@ STATIC_O=$(OBJ_SUFFIX)
+	+$(ICU_MAKE) -C target $@ STATIC_O=$(OBJ_SUFFIX)
 
 endif
 endif
--- a/js/src/Makefile.in
+++ b/js/src/Makefile.in
@@ -1,21 +1,23 @@
 # -*- Mode: makefile -*-
 #
 # 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/.
 
+ifndef .PYMAKE
 ifeq (,$(MAKE_VERSION))
 $(error GNU Make is required)
 endif
 make_min_ver := 3.81
 ifneq ($(make_min_ver),$(firstword $(sort $(make_min_ver) $(MAKE_VERSION))))
 $(error GNU Make $(make_min_ver) or higher is required)
 endif
+endif
 
 TOPLEVEL_BUILD := 1
 
 run_for_side_effects := $(shell echo 'MAKE: $(MAKE)')
 STATIC_LIBRARY_NAME = js_static
 LIBS		= $(NSPR_LIBS)
 
 DIST_INSTALL = 1
--- a/python/mozbuild/mozbuild/base.py
+++ b/python/mozbuild/mozbuild/base.py
@@ -391,32 +391,33 @@ class MozbuildObject(ProcessExecutionMix
 
     def _wrap_path_argument(self, arg):
         return PathArgument(arg, self.topsrcdir, self.topobjdir)
 
     def _run_make(self, directory=None, filename=None, target=None, log=True,
             srcdir=False, allow_parallel=True, line_handler=None,
             append_env=None, explicit_env=None, ignore_errors=False,
             ensure_exit_code=0, silent=True, print_directory=True,
-            pass_thru=False, num_jobs=0):
+            pass_thru=False, num_jobs=0, force_pymake=False):
         """Invoke make.
 
         directory -- Relative directory to look for Makefile in.
         filename -- Explicit makefile to run.
         target -- Makefile target(s) to make. Can be a string or iterable of
             strings.
         srcdir -- If True, invoke make from the source directory tree.
             Otherwise, make will be invoked from the object directory.
         silent -- If True (the default), run make in silent mode.
         print_directory -- If True (the default), have make print directories
         while doing traversal.
+        force_pymake -- If True, pymake will be used instead of GNU make.
         """
         self._ensure_objdir_exists()
 
-        args = self._make_path()
+        args = self._make_path(force_pymake=force_pymake)
 
         if directory:
             args.extend(['-C', directory.replace(os.sep, '/')])
 
         if filename:
             args.extend(['-f', filename])
 
         if allow_parallel:
@@ -429,17 +430,19 @@ class MozbuildObject(ProcessExecutionMix
 
         if ignore_errors:
             args.append('-k')
 
         if silent:
             args.append('-s')
 
         # Print entering/leaving directory messages. Some consumers look at
-        # these to measure progress.
+        # these to measure progress. Ideally, we'd do everything with pymake
+        # and use hooks in its API. Unfortunately, it doesn't provide that
+        # feature... yet.
         if print_directory:
             args.append('-w')
 
         if isinstance(target, list):
             args.extend(target)
         elif target:
             args.append(target)
 
@@ -467,38 +470,54 @@ class MozbuildObject(ProcessExecutionMix
             'ignore_children': True,
         }
 
         if log:
             params['log_name'] = 'make'
 
         return fn(**params)
 
-    def _make_path(self):
-        baseconfig = os.path.join(self.topsrcdir, 'config', 'baseconfig.mk')
-        for test in ('gmake', 'make', 'mozmake', 'gnumake'):
+    def _make_path(self, force_pymake=False):
+        if self._is_windows() and not force_pymake:
+            # Use gnumake if it's available and we can verify it's a working
+            # version.
+            baseconfig = os.path.join(self.topsrcdir, 'config', 'baseconfig.mk')
+            if os.path.exists(baseconfig):
+                try:
+                    make = which.which('gnumake')
+                    subprocess.check_call([make, '-f', baseconfig, 'HOST_OS_ARCH=WINNT'],
+                        stdout=open(os.devnull, 'wb'), stderr=subprocess.STDOUT)
+                    return [make]
+                except subprocess.CalledProcessError:
+                    pass
+                except which.WhichError:
+                    pass
+
+            # Use mozmake if it's available.
             try:
-                make = which.which(test)
-                if os.path.exists(baseconfig):
-                    cmd = [make, '-f', baseconfig]
-                    if self._is_windows():
-                        cmd.append('HOST_OS_ARCH=WINNT')
-                    subprocess.check_call(cmd, stdout=open(os.devnull, 'wb'),
-                        stderr=subprocess.STDOUT)
-                return [make]
-            except subprocess.CalledProcessError:
-                pass
+                return [which.which('mozmake')]
             except which.WhichError:
                 pass
 
-        if self._is_windows():
-            raise Exception('Could not find a suitable make implementation.\n'
-                'Please use MozillaBuild 1.9 or newer')
-        else:
-            raise Exception('Could not find a suitable make implementation.')
+        if self._is_windows() or force_pymake:
+            make_py = os.path.join(self.topsrcdir, 'build', 'pymake',
+                'make.py').replace(os.sep, '/')
+
+            # We might want to consider invoking with the virtualenv's Python
+            # some day. But, there is a chicken-and-egg problem w.r.t. when the
+            # virtualenv is created.
+            return [sys.executable, make_py]
+
+        for test in ['gmake', 'make']:
+            try:
+                return [which.which(test)]
+            except which.WhichError:
+                continue
+
+        raise Exception('Could not find a suitable make implementation.')
 
     def _run_command_in_srcdir(self, **args):
         return self.run_process(cwd=self.topsrcdir, **args)
 
     def _run_command_in_objdir(self, **args):
         return self.run_process(cwd=self.topobjdir, **args)
 
     def _is_windows(self):
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -262,23 +262,25 @@ class BuildOutputManager(LoggingMixin):
 @CommandProvider
 class Build(MachCommandBase):
     """Interface to build the tree."""
 
     @Command('build', category='build', description='Build the tree.')
     @CommandArgument('--jobs', '-j', default='0', metavar='jobs', type=int,
         help='Number of concurrent jobs to run. Default is the number of CPUs.')
     @CommandArgument('what', default=None, nargs='*', help=BUILD_WHAT_HELP)
+    @CommandArgument('-p', '--pymake', action='store_true',
+        help='Force using pymake over GNU make.')
     @CommandArgument('-X', '--disable-extra-make-dependencies',
                      default=False, action='store_true',
                      help='Do not add extra make dependencies.')
     @CommandArgument('-v', '--verbose', action='store_true',
         help='Verbose output for what commands the build is running.')
-    def build(self, what=None, disable_extra_make_dependencies=None, jobs=0,
-        verbose=False):
+    def build(self, what=None, pymake=False,
+        disable_extra_make_dependencies=None, jobs=0, verbose=False):
         import which
         from mozbuild.controller.building import BuildMonitor
         from mozbuild.util import resolve_target_to_make
 
         self.log_manager.register_structured_logger(logging.getLogger('mozbuild'))
 
         warnings_path = self._get_state_filename('warnings.json')
         monitor = self._spawn(BuildMonitor)
@@ -336,47 +338,49 @@ class Build(MachCommandBase):
 
                 # 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.RecursiveMakeBackend',
-                    line_handler=output.on_line, log=False,
-                    print_directory=False)
+                    force_pymake=pymake, line_handler=output.on_line,
+                    log=False, print_directory=False)
 
                 # 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'})
+                        append_env={b'NO_BUILDSTATUS_MESSAGES': b'1'},
+                        force_pymake=pymake)
 
                     if status != 0:
                         break
             else:
                 monitor.start_resource_recording()
                 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)
+                    silent=not verbose, force_pymake=pymake)
 
                 make_extra = self.mozconfig['make_extra'] or []
                 make_extra = dict(m.split('=', 1) for m in make_extra)
 
                 moz_automation = os.getenv('MOZ_AUTOMATION') or make_extra.get('export MOZ_AUTOMATION', None)
                 if moz_automation and status == 0:
                     status = self._run_make(target='automation/build',
                         line_handler=output.on_line, log=False, print_directory=False,
-                        ensure_exit_code=False, num_jobs=jobs, silent=not verbose)
+                        ensure_exit_code=False, num_jobs=jobs, silent=not verbose,
+                        force_pymake=pymake)
 
                 self.log(logging.WARNING, 'warning_summary',
                     {'count': len(monitor.warnings_database)},
                     '{count} compiler warnings present.')
 
             monitor.finish(record_usage=status==0)
 
         high_finder, finder_percent = monitor.have_high_finder_usage()
--- a/security/build/Makefile.in
+++ b/security/build/Makefile.in
@@ -455,16 +455,21 @@ DEFAULT_GMAKE_FLAGS += $(EXTRA_GMAKE_FLA
 $(addprefix libs-,$(NSS_DIRS)): libs-%:
 # Work around NSS's export rule being racy when recursing for private_export
 # See bug #836220.
 $(addprefix export-,$(NSS_DIRS)): EXTRA_GMAKE_FLAGS = PRIVATE_EXPORTS=
 $(addprefix export-,$(NSS_DIRS)): export-%: private_export-%
 $(addprefix private_export-,$(NSS_DIRS)): EXTRA_GMAKE_FLAGS =
 $(addprefix private_export-,$(NSS_DIRS)): private_export-%:
 
+# Work around bug #836228 in pymake
+ifdef .PYMAKE
+$(foreach p,libs export private_export,$(addprefix $(p)-,$(NSS_DIRS))): *=$(subst $(NULL) $(NULL),-,$(wordlist 2,$(words $(subst -, ,$@)),$(subst -, ,$@)))
+endif
+
 $(foreach p,libs export private_export,$(addprefix $(p)-,$(NSS_DIRS))):
 	$(DEFAULT_GMAKE_ENV) $(MAKE) -C $(NSS_SRCDIR)/security/$* $(@:-$*=) $(DEFAULT_GMAKE_FLAGS)
 
 export:: $(addprefix export-,$(NSS_DIRS))
 
 $(addprefix clean-,$(NSS_DIRS)): clean-%:
 	$(MAKE) -C $(NSS_SRCDIR)/security/$* $(DEFAULT_GMAKE_FLAGS) clean