Bug 905973 part 3 - Add a "binaries" tier that optimizes for recompilation times. r=gps
authorMike Hommey <mh+mozilla@glandium.org>
Wed, 02 Oct 2013 08:53:23 +0900
changeset 149583 6be8c784235c92a7c72c1cf9cc797cf564cc8335
parent 149582 99e29833c316db196c00b250e654256e8b9dd19b
child 149584 c5906eed61fcc7a83f8ed23cdbbc868d6c45aaaf
push id25395
push useremorley@mozilla.com
push dateWed, 02 Oct 2013 15:42:23 +0000
treeherdermozilla-central@8bc54ab0ef14 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs905973
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 905973 part 3 - Add a "binaries" tier that optimizes for recompilation times. r=gps
Makefile.in
build/docs/build-targets.rst
config/makefiles/target_libs.mk
config/recurse.mk
config/rules.mk
js/src/config/makefiles/target_libs.mk
js/src/config/recurse.mk
js/src/config/rules.mk
python/mozbuild/mozbuild/backend/recursivemake.py
--- a/Makefile.in
+++ b/Makefile.in
@@ -28,19 +28,19 @@ DIST_GARBAGE = config.cache config.log c
    config/autoconf.mk \
    mozilla-config.h \
    netwerk/necko-config.h xpcom/xpcom-config.h xpcom/xpcom-private.h \
    .mozconfig.mk
 
 ifndef MOZ_PROFILE_USE
 # We need to explicitly put backend.RecursiveMakeBackend.built here
 # otherwise the rule in rules.mk doesn't run early enough.
-libs export tools:: CLOBBER $(topsrcdir)/configure config.status backend.RecursiveMakeBackend.built
+libs binaries export tools:: CLOBBER $(topsrcdir)/configure config.status backend.RecursiveMakeBackend.built
 ifndef LIBXUL_SDK
-libs export tools:: js-config-status
+libs binaries export tools:: js-config-status
 endif
 endif
 
 CLOBBER: $(topsrcdir)/CLOBBER
 	@echo "STOP!  The CLOBBER file has changed."
 	@echo "Please run the build through a sanctioned build wrapper, such as"
 	@echo "'mach build' or client.mk."
 	@exit 1
@@ -98,20 +98,24 @@ endif
 
 # Windows PGO builds don't perform a clean before the 2nd pass. So, we want
 # to preserve content for the 2nd pass on Windows. Everywhere else, we always
 # process the install manifests as part of export.
 ifdef MOZ_PROFILE_USE
 ifndef NO_PROFILE_GUIDED_OPTIMIZE
 ifneq ($(OS_ARCH)_$(GNU_CC), WINNT_)
 export:: install-manifests
+binaries::
+	@$(MAKE) install-manifests NO_REMOVE=1
 endif
 endif
 else # !MOZ_PROFILE_USE (normal build)
 export:: install-manifests
+binaries::
+	@$(MAKE) install-manifests NO_REMOVE=1
 endif
 
 # For historical reasons that are unknown, $(DIST)/sdk is always blown away
 # with no regard for PGO passes. This decision could probably be revisited.
 export:: install-dist-sdk
 
 ifdef ENABLE_TESTS
 # Additional makefile targets to call automated test suites
@@ -234,10 +238,15 @@ check::
 	$(call SUBMAKE,$@,js/src)
 endif
 
 ifdef MOZ_PSEUDO_DERECURSE
 # Interdependencies for parallel export.
 js/xpconnect/src/export: dom/bindings/export
 accessible/src/xpcom/export: xpcom/xpidl/export
 js/src/export: mfbt/export
+# Temporary interdependencies for binaries, until bug 921307 is fixed.
+# Avoid traversal of those directories when creating the aggregate dependency file
+ifneq (binaries-deps,$(MAKECMDGOALS))
+toolkit/library/binaries: js/src/binaries media/webrtc/trunk/binaries media/webrtc/signaling/binaries media/webrtc/trunk/testing/binaries media/webrtc/signalingtest/binaries media/mtransport/third_party/nrappkit/binaries media/mtransport/third_party/nICEr/binaries
 endif
 endif
+endif
--- a/build/docs/build-targets.rst
+++ b/build/docs/build-targets.rst
@@ -15,26 +15,34 @@ partial tree builds can be unreliable. U
 
 export
    Build the *export* tier. The *export* tier builds everything that is
    required for C/C++ compilation. It stages all header files, processes
    IDLs, etc.
 
 compile
    Build the *compile* tier. The *compile* tier compiles all C/C++ files.
+   Only applies to builds with ``MOZ_PSEUDO_DERECURSE``.
 
 libs
    Build the *libs* tier. The *libs* tier performs linking and performs
    most build steps which aren't related to compilation.
 
 tools
    Build the *tools* tier. The *tools* tier mostly deals with supplementary
    tools and compiled tests. It will link tools against libXUL, including
    compiled test binaries.
 
+binaries:
+   Recompiles and relinks C/C++ files. Only works after a complete normal
+   build, but allows for much faster rebuilds of C/C++ code. For performance
+   reasons, however, it skips nss, nspr, icu and ffi. This is targeted to
+   improve local developer workflow when touching C/C++ code.
+   Only applies to builds with ``MOZ_PSEUDO_DERECURSE``.
+
 install-manifests
    Process install manifests. Install manifests handle the installation of
    files into the object directory.
 
    Unless ``NO_REMOVE=1`` is defined in the environment, files not accounted
    in the install manifests will be deleted from the object directory.
 
 install-tests
--- a/config/makefiles/target_libs.mk
+++ b/config/makefiles/target_libs.mk
@@ -13,17 +13,17 @@ else
 EXPORT_LIBRARY = $(DEPTH)/staticlib
 endif
 else
 # If EXPORT_LIBRARY has a value, we'll be installing there. We also need to cleanup there
 GARBAGE += $(foreach lib,$(LIBRARY),$(EXPORT_LIBRARY)/$(lib))
 endif
 endif # EXPORT_LIBRARY
 
-libs:: $(SUBMAKEFILES) $(HOST_LIBRARY) $(LIBRARY) $(SHARED_LIBRARY) $(IMPORT_LIBRARY) $(HOST_PROGRAM) $(HOST_SIMPLE_PROGRAMS) $(SIMPLE_PROGRAMS)
+binaries libs:: $(SUBMAKEFILES) $(TARGETS)
 ifndef NO_DIST_INSTALL
 ifdef SHARED_LIBRARY
 ifdef IS_COMPONENT
 	$(INSTALL) $(IFLAGS2) $(SHARED_LIBRARY) $(FINAL_TARGET)/components
 	$(ELF_DYNSTR_GC) $(FINAL_TARGET)/components/$(SHARED_LIBRARY)
 ifndef NO_COMPONENTS_MANIFEST
 	@$(PYTHON) $(MOZILLA_DIR)/config/buildlist.py $(FINAL_TARGET)/chrome.manifest "manifest components/components.manifest"
 	@$(PYTHON) $(MOZILLA_DIR)/config/buildlist.py $(FINAL_TARGET)/components/components.manifest "binary-component $(SHARED_LIBRARY)"
@@ -32,66 +32,105 @@ endif # IS_COMPONENT
 endif # SHARED_LIBRARY
 endif # !NO_DIST_INSTALL
 
 ifndef NO_DIST_INSTALL
 
 ifneq (,$(strip $(PROGRAM)$(SIMPLE_PROGRAMS)))
 PROGRAMS_EXECUTABLES = $(SIMPLE_PROGRAMS) $(PROGRAM)
 PROGRAMS_DEST ?= $(FINAL_TARGET)
+PROGRAMS_TARGET := binaries libs
 INSTALL_TARGETS += PROGRAMS
 endif
 
 ifdef LIBRARY
 ifdef EXPORT_LIBRARY
 LIBRARY_FILES = $(LIBRARY)
 LIBRARY_DEST ?= $(EXPORT_LIBRARY)
+LIBRARY_TARGET = binaries libs
 INSTALL_TARGETS += LIBRARY
 endif
 ifdef DIST_INSTALL
 ifdef IS_COMPONENT
 $(error Shipping static component libs makes no sense.)
 else
 DIST_LIBRARY_FILES = $(LIBRARY)
 DIST_LIBRARY_DEST ?= $(DIST)/lib
+DIST_LIBRARY_TARGET = binaries libs
 INSTALL_TARGETS += DIST_LIBRARY
 endif
 endif # DIST_INSTALL
 endif # LIBRARY
 
 
 ifdef SHARED_LIBRARY
 ifndef IS_COMPONENT
 SHARED_LIBRARY_FILES = $(SHARED_LIBRARY)
 SHARED_LIBRARY_DEST ?= $(FINAL_TARGET)
+SHARED_LIBRARY_TARGET = binaries libs
 INSTALL_TARGETS += SHARED_LIBRARY
 
 ifneq (,$(filter OS2 WINNT,$(OS_ARCH)))
 ifndef NO_INSTALL_IMPORT_LIBRARY
 IMPORT_LIB_FILES = $(IMPORT_LIBRARY)
 endif # NO_INSTALL_IMPORT_LIBRARY
 else
 IMPORT_LIB_FILES = $(SHARED_LIBRARY)
 endif
 
 IMPORT_LIB_DEST ?= $(DIST)/lib
+IMPORT_LIB_TARGET = binaries libs
 ifdef IMPORT_LIB_FILES
 INSTALL_TARGETS += IMPORT_LIB
 endif
 
 endif # ! IS_COMPONENT
 endif # SHARED_LIBRARY
 
 ifneq (,$(strip $(HOST_SIMPLE_PROGRAMS)$(HOST_PROGRAM)))
 HOST_PROGRAMS_EXECUTABLES = $(HOST_SIMPLE_PROGRAMS) $(HOST_PROGRAM)
 HOST_PROGRAMS_DEST ?= $(DIST)/host/bin
+HOST_PROGRAMS_TARGET = binaries libs
 INSTALL_TARGETS += HOST_PROGRAMS
 endif
 
 ifdef HOST_LIBRARY
 HOST_LIBRARY_FILES = $(HOST_LIBRARY)
 HOST_LIBRARY_DEST ?= $(DIST)/host/lib
+HOST_LIBRARY_TARGET = binaries libs
 INSTALL_TARGETS += HOST_LIBRARY
 endif
 
 endif # !NO_DIST_INSTALL
 
+ifdef MOZ_PSEUDO_DERECURSE
+BINARIES_INSTALL_TARGETS := $(foreach category,$(INSTALL_TARGETS),$(if $(filter binaries,$($(category)_TARGET)),$(category)))
+
+ifneq (,$(strip $(BINARIES_INSTALL_TARGETS)))
+# Fill a dependency file with all the binaries installed somewhere in $(DIST)
+BINARIES_PP := $(MDDEPDIR)/binaries.pp
+
+$(BINARIES_PP): Makefile backend.mk $(call mkdir_deps,$(MDDEPDIR))
+	@echo "$(strip $(foreach category,$(BINARIES_INSTALL_TARGETS),\
+		$(foreach file,$($(category)_FILES) $($(category)_EXECUTABLES),\
+			$($(category)_DEST)/$(notdir $(file)): $(file)%\
+		)\
+	))" | tr % '\n' > $@
+endif
+
+binaries libs:: $(TARGETS) $(BINARIES_PP)
+# Aggregate all dependency files relevant to a binaries build. If there is nothing
+# done in the current directory, just create an empty stamp.
+# Externally managed make files (gyp managed) and root make files (js/src/Makefile)
+# need to be recursed to do their duty, and creating a stamp would prevent that.
+# In the future, we'll aggregate those.
+ifneq (.,$(DEPTH))
+ifndef EXTERNALLY_MANAGED_MAKE_FILE
+	@$(if $^,$(call py_action,link_deps,-o binaries --group-all --topsrcdir $(topsrcdir) --topobjdir $(DEPTH) --dist $(DIST) $(BINARIES_PP) $(wildcard $(addsuffix .pp,$(addprefix $(MDDEPDIR)/,$(notdir $(sort $(filter-out $(BINARIES_PP),$^) $(OBJ_TARGETS))))))),$(TOUCH) binaries)
+endif
+endif
+
+else
+binaries::
+	$(error The binaries target is not supported without MOZ_PSEUDO_DERECURSE)
+endif
+
 # EOF
--- a/config/recurse.mk
+++ b/config/recurse.mk
@@ -28,31 +28,31 @@ ifeq (1_.,$(if $(MOZ_PSEUDO_DERECURSE),1
 include root.mk
 
 # Disable build status for mach in top directories without TIERS.
 # In practice this disables it when recursing under js/src, which confuses mach.
 ifndef TIERS
 BUILDSTATUS =
 endif
 
-# Main rules (export, compile, libs and tools) call recurse_* rules.
+# Main rules (export, compile, binaries, libs and tools) call recurse_* rules.
 # This wrapping is only really useful for build status.
-compile libs export tools::
+compile binaries libs export tools::
 	$(call BUILDSTATUS,TIER_START $@ $($@_subtiers))
 	+$(MAKE) recurse_$@
 	$(call BUILDSTATUS,TIER_FINISH $@)
 
 # Carefully avoid $(eval) type of rule generation, which makes pymake slower
 # than necessary.
 # Get current tier and corresponding subtiers from the data in root.mk.
-CURRENT_TIER := $(filter $(foreach tier,compile libs export tools,recurse_$(tier)),$(MAKECMDGOALS))
+CURRENT_TIER := $(filter $(foreach tier,compile binaries libs export tools,recurse_$(tier) $(tier)-deps),$(MAKECMDGOALS))
 ifneq (,$(filter-out 0 1,$(words $(CURRENT_TIER))))
 $(error $(CURRENT_TIER) not supported on the same make command line)
 endif
-CURRENT_TIER := $(subst recurse_,,$(CURRENT_TIER))
+CURRENT_TIER := $(subst recurse_,,$(CURRENT_TIER:-deps=))
 CURRENT_SUBTIERS := $($(CURRENT_TIER)_subtiers)
 
 # The rules here are doing directory traversal, so we don't want further
 # recursion to happen when running make -C subdir $tier. But some make files
 # further call make -C something else, and sometimes expect recursion to
 # happen in that case (see browser/metro/locales/Makefile.in for example).
 # Conveniently, every invocation of make increases MAKELEVEL, so only stop
 # recursion from happening at current MAKELEVEL + 1.
@@ -64,49 +64,93 @@ export NO_RECURSE_MAKELEVEL=$(word $(MAK
 endif
 endif
 
 # Get all directories traversed for all subtiers in the current tier, or use
 # directly the $(*_dirs) variables available in root.mk when there is no
 # TIERS (like for js/src).
 CURRENT_DIRS := $(or $($(CURRENT_TIER)_dirs),$(foreach subtier,$(CURRENT_SUBTIERS),$($(CURRENT_TIER)_subtier_$(subtier))))
 
+ifneq (,$(filter binaries libs,$(CURRENT_TIER)))
+WANT_STAMPS = 1
+STAMP_TOUCH = $(TOUCH) $(@D)/binaries
+endif
+
 # Subtier delimiter rules
-$(addprefix subtiers/,$(addsuffix _start/$(CURRENT_TIER),$(CURRENT_SUBTIERS))): subtiers/%_start/$(CURRENT_TIER):
+$(addprefix subtiers/,$(addsuffix _start/$(CURRENT_TIER),$(CURRENT_SUBTIERS))): subtiers/%_start/$(CURRENT_TIER): $(if $(WANT_STAMPS),$(call mkdir_deps,subtiers/%_start))
 	$(call BUILDSTATUS,SUBTIER_START $(CURRENT_TIER) $* $(if $(BUG_915535_FIXED),$($(CURRENT_TIER)_subtier_$*)))
+	@$(STAMP_TOUCH)
 
-$(addprefix subtiers/,$(addsuffix _finish/$(CURRENT_TIER),$(CURRENT_SUBTIERS))): subtiers/%_finish/$(CURRENT_TIER):
+$(addprefix subtiers/,$(addsuffix _finish/$(CURRENT_TIER),$(CURRENT_SUBTIERS))): subtiers/%_finish/$(CURRENT_TIER): $(if $(WANT_STAMPS),$(call mkdir_deps,subtiers/%_finish))
 	$(call BUILDSTATUS,SUBTIER_FINISH $(CURRENT_TIER) $*)
+	@$(STAMP_TOUCH)
+
+$(addprefix subtiers/,$(addsuffix /$(CURRENT_TIER),$(CURRENT_SUBTIERS))): %/$(CURRENT_TIER): $(if $(WANT_STAMPS),$(call mkdir_deps,%))
+	@$(STAMP_TOUCH)
+
+GARBAGE_DIRS += subtiers
 
 # Recursion rule for all directories traversed for all subtiers in the
 # current tier.
 # root.mk defines subtier_of_* variables, that map a normalized subdir path to
 # a subtier name (e.g. subtier_of_memory_jemalloc = base)
-$(addsuffix /$(CURRENT_TIER),$(CURRENT_DIRS)): %/$(CURRENT_TIER):
+$(addsuffix /$(CURRENT_TIER),$(CURRENT_DIRS)): %/$(CURRENT_TIER): $(if $(WANT_STAMPS),%/Makefile %/backend.mk)
 ifdef BUG_915535_FIXED
 	$(call BUILDSTATUS,TIERDIR_START $(CURRENT_TIER) $(subtier_of_$(subst /,_,$*)) $*)
 endif
 	+@$(MAKE) -C $* $(if $(filter $*,$(tier_$(subtier_of_$(subst /,_,$*))_staticdirs)),,$(CURRENT_TIER))
+# Ensure existing stamps are up-to-date, but don't create one if submake didn't create one.
+	$(if $(wildcard $@),@$(STAMP_TOUCH))
 ifdef BUG_915535_FIXED
 	$(call BUILDSTATUS,TIERDIR_FINISH $(CURRENT_TIER) $(subtier_of_$(subst /,_,$*)) $*)
 endif
 
+# Dummy rules for possibly inexisting dependencies for the above tier targets
+$(addsuffix /Makefile,$(CURRENT_DIRS)) $(addsuffix /backend.mk,$(CURRENT_DIRS)):
+
 # The export tier requires nsinstall, which is built from config. So every
 # subdirectory traversal needs to happen after traversing config.
 ifeq ($(CURRENT_TIER),export)
 $(addsuffix /$(CURRENT_TIER),$(filter-out config,$(CURRENT_DIRS))): config/$(CURRENT_TIER)
 endif
 
+ifneq (,$(filter libs binaries,$(CURRENT_TIER)))
+# When doing a "libs" build, target_libs.mk ensures the interesting dependency data
+# is available in the "binaries" stamp. Once recursion is done, aggregate all that
+# dependency info so that stamps depend on relevant files and relevant other stamps.
+# When doing a "binaries" build, the aggregate dependency file and those stamps are
+# used and allow to skip recursing directories where changes are not going to require
+# rebuild. A few directories, however, are still traversed all the time, mostly, the
+# gyp managed ones and js/src.
+# A few things that are not traversed by a "binaries" build, but should, in an ideal
+# world, are nspr, nss, icu and ffi.
+recurse_$(CURRENT_TIER):
+	@$(MAKE) binaries-deps
+
+# Creating binaries-deps.mk directly would make us build it twice: once when beginning
+# the build because of the include, and once at the end because of the stamps.
+binaries-deps: $(wildcard $(addsuffix /binaries,$(CURRENT_DIRS)))
+	@$(call py_action,link_deps,-o $@.mk --group-by-depfile --topsrcdir $(topsrcdir) --topobjdir $(DEPTH) --dist $(DIST) --guard $(addprefix ",$(addsuffix ",$^)))
+	@$(TOUCH) $@
+
+ifeq (recurse_binaries,$(MAKECMDGOALS))
+$(call include_deps,binaries-deps.mk)
+endif
+
+endif
+
+DIST_GARBAGE += binaries-deps.mk binaries-deps
+
 else
 
 # Don't recurse if MAKELEVEL is NO_RECURSE_MAKELEVEL as defined above, but
 # still recurse for externally managed make files (gyp-generated ones).
 ifeq ($(EXTERNALLY_MANAGED_MAKE_FILE)_$(NO_RECURSE_MAKELEVEL),_$(MAKELEVEL))
 
-compile libs export tools::
+compile binaries libs export tools::
 
 else
 #########################
 # Tier traversal handling
 #########################
 
 ifdef TIERS
 
@@ -134,17 +178,17 @@ endif
 $(1):: $$(SUBMAKEFILES)
 ifdef PARALLEL_DIRS
 	+@$(MAKE) $$(PARALLEL_DIRS_$(1))
 endif
 	$$(LOOP_OVER_DIRS)
 
 endef
 
-$(foreach subtier,export libs tools,$(eval $(call CREATE_SUBTIER_TRAVERSAL_RULE,$(subtier))))
+$(foreach subtier,export compile binaries libs tools,$(eval $(call CREATE_SUBTIER_TRAVERSAL_RULE,$(subtier))))
 
 tools export:: $(SUBMAKEFILES)
 	$(LOOP_OVER_TOOL_DIRS)
 
 endif # ifdef TIERS
 
 endif # ifeq ($(EXTERNALLY_MANAGED_MAKE_FILE)_$(NO_RECURSE_MAKELEVEL),_$(MAKELEVEL))
 
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -724,17 +724,19 @@ HOST_LIBS_DEPS = $(filter %.$(LIB_SUFFIX
 
 # Dependencies which, if modified, should cause everything to rebuild
 GLOBAL_DEPS += Makefile $(DEPTH)/config/autoconf.mk $(topsrcdir)/config/config.mk
 ifndef NO_MAKEFILE_RULE
 GLOBAL_DEPS += Makefile.in
 endif
 
 ##############################################
-compile:: $(OBJS) $(HOST_OBJS)
+OBJ_TARGETS = $(OBJS) $(PROGOBJS) $(HOST_OBJS) $(HOST_PROGOBJS)
+
+compile:: $(OBJ_TARGETS)
 
 include $(topsrcdir)/config/makefiles/target_libs.mk
 
 ifdef IS_TOOL_DIR
 # One would think "tools:: libs" would work, but it turns out that combined with
 # bug 907365, this makes make forget to run some rules sometimes.
 tools::
 	@$(MAKE) libs
--- a/js/src/config/makefiles/target_libs.mk
+++ b/js/src/config/makefiles/target_libs.mk
@@ -13,17 +13,17 @@ else
 EXPORT_LIBRARY = $(DEPTH)/staticlib
 endif
 else
 # If EXPORT_LIBRARY has a value, we'll be installing there. We also need to cleanup there
 GARBAGE += $(foreach lib,$(LIBRARY),$(EXPORT_LIBRARY)/$(lib))
 endif
 endif # EXPORT_LIBRARY
 
-libs:: $(SUBMAKEFILES) $(HOST_LIBRARY) $(LIBRARY) $(SHARED_LIBRARY) $(IMPORT_LIBRARY) $(HOST_PROGRAM) $(HOST_SIMPLE_PROGRAMS) $(SIMPLE_PROGRAMS)
+binaries libs:: $(SUBMAKEFILES) $(TARGETS)
 ifndef NO_DIST_INSTALL
 ifdef SHARED_LIBRARY
 ifdef IS_COMPONENT
 	$(INSTALL) $(IFLAGS2) $(SHARED_LIBRARY) $(FINAL_TARGET)/components
 	$(ELF_DYNSTR_GC) $(FINAL_TARGET)/components/$(SHARED_LIBRARY)
 ifndef NO_COMPONENTS_MANIFEST
 	@$(PYTHON) $(MOZILLA_DIR)/config/buildlist.py $(FINAL_TARGET)/chrome.manifest "manifest components/components.manifest"
 	@$(PYTHON) $(MOZILLA_DIR)/config/buildlist.py $(FINAL_TARGET)/components/components.manifest "binary-component $(SHARED_LIBRARY)"
@@ -32,66 +32,105 @@ endif # IS_COMPONENT
 endif # SHARED_LIBRARY
 endif # !NO_DIST_INSTALL
 
 ifndef NO_DIST_INSTALL
 
 ifneq (,$(strip $(PROGRAM)$(SIMPLE_PROGRAMS)))
 PROGRAMS_EXECUTABLES = $(SIMPLE_PROGRAMS) $(PROGRAM)
 PROGRAMS_DEST ?= $(FINAL_TARGET)
+PROGRAMS_TARGET := binaries libs
 INSTALL_TARGETS += PROGRAMS
 endif
 
 ifdef LIBRARY
 ifdef EXPORT_LIBRARY
 LIBRARY_FILES = $(LIBRARY)
 LIBRARY_DEST ?= $(EXPORT_LIBRARY)
+LIBRARY_TARGET = binaries libs
 INSTALL_TARGETS += LIBRARY
 endif
 ifdef DIST_INSTALL
 ifdef IS_COMPONENT
 $(error Shipping static component libs makes no sense.)
 else
 DIST_LIBRARY_FILES = $(LIBRARY)
 DIST_LIBRARY_DEST ?= $(DIST)/lib
+DIST_LIBRARY_TARGET = binaries libs
 INSTALL_TARGETS += DIST_LIBRARY
 endif
 endif # DIST_INSTALL
 endif # LIBRARY
 
 
 ifdef SHARED_LIBRARY
 ifndef IS_COMPONENT
 SHARED_LIBRARY_FILES = $(SHARED_LIBRARY)
 SHARED_LIBRARY_DEST ?= $(FINAL_TARGET)
+SHARED_LIBRARY_TARGET = binaries libs
 INSTALL_TARGETS += SHARED_LIBRARY
 
 ifneq (,$(filter OS2 WINNT,$(OS_ARCH)))
 ifndef NO_INSTALL_IMPORT_LIBRARY
 IMPORT_LIB_FILES = $(IMPORT_LIBRARY)
 endif # NO_INSTALL_IMPORT_LIBRARY
 else
 IMPORT_LIB_FILES = $(SHARED_LIBRARY)
 endif
 
 IMPORT_LIB_DEST ?= $(DIST)/lib
+IMPORT_LIB_TARGET = binaries libs
 ifdef IMPORT_LIB_FILES
 INSTALL_TARGETS += IMPORT_LIB
 endif
 
 endif # ! IS_COMPONENT
 endif # SHARED_LIBRARY
 
 ifneq (,$(strip $(HOST_SIMPLE_PROGRAMS)$(HOST_PROGRAM)))
 HOST_PROGRAMS_EXECUTABLES = $(HOST_SIMPLE_PROGRAMS) $(HOST_PROGRAM)
 HOST_PROGRAMS_DEST ?= $(DIST)/host/bin
+HOST_PROGRAMS_TARGET = binaries libs
 INSTALL_TARGETS += HOST_PROGRAMS
 endif
 
 ifdef HOST_LIBRARY
 HOST_LIBRARY_FILES = $(HOST_LIBRARY)
 HOST_LIBRARY_DEST ?= $(DIST)/host/lib
+HOST_LIBRARY_TARGET = binaries libs
 INSTALL_TARGETS += HOST_LIBRARY
 endif
 
 endif # !NO_DIST_INSTALL
 
+ifdef MOZ_PSEUDO_DERECURSE
+BINARIES_INSTALL_TARGETS := $(foreach category,$(INSTALL_TARGETS),$(if $(filter binaries,$($(category)_TARGET)),$(category)))
+
+ifneq (,$(strip $(BINARIES_INSTALL_TARGETS)))
+# Fill a dependency file with all the binaries installed somewhere in $(DIST)
+BINARIES_PP := $(MDDEPDIR)/binaries.pp
+
+$(BINARIES_PP): Makefile backend.mk $(call mkdir_deps,$(MDDEPDIR))
+	@echo "$(strip $(foreach category,$(BINARIES_INSTALL_TARGETS),\
+		$(foreach file,$($(category)_FILES) $($(category)_EXECUTABLES),\
+			$($(category)_DEST)/$(notdir $(file)): $(file)%\
+		)\
+	))" | tr % '\n' > $@
+endif
+
+binaries libs:: $(TARGETS) $(BINARIES_PP)
+# Aggregate all dependency files relevant to a binaries build. If there is nothing
+# done in the current directory, just create an empty stamp.
+# Externally managed make files (gyp managed) and root make files (js/src/Makefile)
+# need to be recursed to do their duty, and creating a stamp would prevent that.
+# In the future, we'll aggregate those.
+ifneq (.,$(DEPTH))
+ifndef EXTERNALLY_MANAGED_MAKE_FILE
+	@$(if $^,$(call py_action,link_deps,-o binaries --group-all --topsrcdir $(topsrcdir) --topobjdir $(DEPTH) --dist $(DIST) $(BINARIES_PP) $(wildcard $(addsuffix .pp,$(addprefix $(MDDEPDIR)/,$(notdir $(sort $(filter-out $(BINARIES_PP),$^) $(OBJ_TARGETS))))))),$(TOUCH) binaries)
+endif
+endif
+
+else
+binaries::
+	$(error The binaries target is not supported without MOZ_PSEUDO_DERECURSE)
+endif
+
 # EOF
--- a/js/src/config/recurse.mk
+++ b/js/src/config/recurse.mk
@@ -28,31 +28,31 @@ ifeq (1_.,$(if $(MOZ_PSEUDO_DERECURSE),1
 include root.mk
 
 # Disable build status for mach in top directories without TIERS.
 # In practice this disables it when recursing under js/src, which confuses mach.
 ifndef TIERS
 BUILDSTATUS =
 endif
 
-# Main rules (export, compile, libs and tools) call recurse_* rules.
+# Main rules (export, compile, binaries, libs and tools) call recurse_* rules.
 # This wrapping is only really useful for build status.
-compile libs export tools::
+compile binaries libs export tools::
 	$(call BUILDSTATUS,TIER_START $@ $($@_subtiers))
 	+$(MAKE) recurse_$@
 	$(call BUILDSTATUS,TIER_FINISH $@)
 
 # Carefully avoid $(eval) type of rule generation, which makes pymake slower
 # than necessary.
 # Get current tier and corresponding subtiers from the data in root.mk.
-CURRENT_TIER := $(filter $(foreach tier,compile libs export tools,recurse_$(tier)),$(MAKECMDGOALS))
+CURRENT_TIER := $(filter $(foreach tier,compile binaries libs export tools,recurse_$(tier) $(tier)-deps),$(MAKECMDGOALS))
 ifneq (,$(filter-out 0 1,$(words $(CURRENT_TIER))))
 $(error $(CURRENT_TIER) not supported on the same make command line)
 endif
-CURRENT_TIER := $(subst recurse_,,$(CURRENT_TIER))
+CURRENT_TIER := $(subst recurse_,,$(CURRENT_TIER:-deps=))
 CURRENT_SUBTIERS := $($(CURRENT_TIER)_subtiers)
 
 # The rules here are doing directory traversal, so we don't want further
 # recursion to happen when running make -C subdir $tier. But some make files
 # further call make -C something else, and sometimes expect recursion to
 # happen in that case (see browser/metro/locales/Makefile.in for example).
 # Conveniently, every invocation of make increases MAKELEVEL, so only stop
 # recursion from happening at current MAKELEVEL + 1.
@@ -64,49 +64,93 @@ export NO_RECURSE_MAKELEVEL=$(word $(MAK
 endif
 endif
 
 # Get all directories traversed for all subtiers in the current tier, or use
 # directly the $(*_dirs) variables available in root.mk when there is no
 # TIERS (like for js/src).
 CURRENT_DIRS := $(or $($(CURRENT_TIER)_dirs),$(foreach subtier,$(CURRENT_SUBTIERS),$($(CURRENT_TIER)_subtier_$(subtier))))
 
+ifneq (,$(filter binaries libs,$(CURRENT_TIER)))
+WANT_STAMPS = 1
+STAMP_TOUCH = $(TOUCH) $(@D)/binaries
+endif
+
 # Subtier delimiter rules
-$(addprefix subtiers/,$(addsuffix _start/$(CURRENT_TIER),$(CURRENT_SUBTIERS))): subtiers/%_start/$(CURRENT_TIER):
+$(addprefix subtiers/,$(addsuffix _start/$(CURRENT_TIER),$(CURRENT_SUBTIERS))): subtiers/%_start/$(CURRENT_TIER): $(if $(WANT_STAMPS),$(call mkdir_deps,subtiers/%_start))
 	$(call BUILDSTATUS,SUBTIER_START $(CURRENT_TIER) $* $(if $(BUG_915535_FIXED),$($(CURRENT_TIER)_subtier_$*)))
+	@$(STAMP_TOUCH)
 
-$(addprefix subtiers/,$(addsuffix _finish/$(CURRENT_TIER),$(CURRENT_SUBTIERS))): subtiers/%_finish/$(CURRENT_TIER):
+$(addprefix subtiers/,$(addsuffix _finish/$(CURRENT_TIER),$(CURRENT_SUBTIERS))): subtiers/%_finish/$(CURRENT_TIER): $(if $(WANT_STAMPS),$(call mkdir_deps,subtiers/%_finish))
 	$(call BUILDSTATUS,SUBTIER_FINISH $(CURRENT_TIER) $*)
+	@$(STAMP_TOUCH)
+
+$(addprefix subtiers/,$(addsuffix /$(CURRENT_TIER),$(CURRENT_SUBTIERS))): %/$(CURRENT_TIER): $(if $(WANT_STAMPS),$(call mkdir_deps,%))
+	@$(STAMP_TOUCH)
+
+GARBAGE_DIRS += subtiers
 
 # Recursion rule for all directories traversed for all subtiers in the
 # current tier.
 # root.mk defines subtier_of_* variables, that map a normalized subdir path to
 # a subtier name (e.g. subtier_of_memory_jemalloc = base)
-$(addsuffix /$(CURRENT_TIER),$(CURRENT_DIRS)): %/$(CURRENT_TIER):
+$(addsuffix /$(CURRENT_TIER),$(CURRENT_DIRS)): %/$(CURRENT_TIER): $(if $(WANT_STAMPS),%/Makefile %/backend.mk)
 ifdef BUG_915535_FIXED
 	$(call BUILDSTATUS,TIERDIR_START $(CURRENT_TIER) $(subtier_of_$(subst /,_,$*)) $*)
 endif
 	+@$(MAKE) -C $* $(if $(filter $*,$(tier_$(subtier_of_$(subst /,_,$*))_staticdirs)),,$(CURRENT_TIER))
+# Ensure existing stamps are up-to-date, but don't create one if submake didn't create one.
+	$(if $(wildcard $@),@$(STAMP_TOUCH))
 ifdef BUG_915535_FIXED
 	$(call BUILDSTATUS,TIERDIR_FINISH $(CURRENT_TIER) $(subtier_of_$(subst /,_,$*)) $*)
 endif
 
+# Dummy rules for possibly inexisting dependencies for the above tier targets
+$(addsuffix /Makefile,$(CURRENT_DIRS)) $(addsuffix /backend.mk,$(CURRENT_DIRS)):
+
 # The export tier requires nsinstall, which is built from config. So every
 # subdirectory traversal needs to happen after traversing config.
 ifeq ($(CURRENT_TIER),export)
 $(addsuffix /$(CURRENT_TIER),$(filter-out config,$(CURRENT_DIRS))): config/$(CURRENT_TIER)
 endif
 
+ifneq (,$(filter libs binaries,$(CURRENT_TIER)))
+# When doing a "libs" build, target_libs.mk ensures the interesting dependency data
+# is available in the "binaries" stamp. Once recursion is done, aggregate all that
+# dependency info so that stamps depend on relevant files and relevant other stamps.
+# When doing a "binaries" build, the aggregate dependency file and those stamps are
+# used and allow to skip recursing directories where changes are not going to require
+# rebuild. A few directories, however, are still traversed all the time, mostly, the
+# gyp managed ones and js/src.
+# A few things that are not traversed by a "binaries" build, but should, in an ideal
+# world, are nspr, nss, icu and ffi.
+recurse_$(CURRENT_TIER):
+	@$(MAKE) binaries-deps
+
+# Creating binaries-deps.mk directly would make us build it twice: once when beginning
+# the build because of the include, and once at the end because of the stamps.
+binaries-deps: $(wildcard $(addsuffix /binaries,$(CURRENT_DIRS)))
+	@$(call py_action,link_deps,-o $@.mk --group-by-depfile --topsrcdir $(topsrcdir) --topobjdir $(DEPTH) --dist $(DIST) --guard $(addprefix ",$(addsuffix ",$^)))
+	@$(TOUCH) $@
+
+ifeq (recurse_binaries,$(MAKECMDGOALS))
+$(call include_deps,binaries-deps.mk)
+endif
+
+endif
+
+DIST_GARBAGE += binaries-deps.mk binaries-deps
+
 else
 
 # Don't recurse if MAKELEVEL is NO_RECURSE_MAKELEVEL as defined above, but
 # still recurse for externally managed make files (gyp-generated ones).
 ifeq ($(EXTERNALLY_MANAGED_MAKE_FILE)_$(NO_RECURSE_MAKELEVEL),_$(MAKELEVEL))
 
-compile libs export tools::
+compile binaries libs export tools::
 
 else
 #########################
 # Tier traversal handling
 #########################
 
 ifdef TIERS
 
@@ -134,17 +178,17 @@ endif
 $(1):: $$(SUBMAKEFILES)
 ifdef PARALLEL_DIRS
 	+@$(MAKE) $$(PARALLEL_DIRS_$(1))
 endif
 	$$(LOOP_OVER_DIRS)
 
 endef
 
-$(foreach subtier,export libs tools,$(eval $(call CREATE_SUBTIER_TRAVERSAL_RULE,$(subtier))))
+$(foreach subtier,export compile binaries libs tools,$(eval $(call CREATE_SUBTIER_TRAVERSAL_RULE,$(subtier))))
 
 tools export:: $(SUBMAKEFILES)
 	$(LOOP_OVER_TOOL_DIRS)
 
 endif # ifdef TIERS
 
 endif # ifeq ($(EXTERNALLY_MANAGED_MAKE_FILE)_$(NO_RECURSE_MAKELEVEL),_$(MAKELEVEL))
 
--- a/js/src/config/rules.mk
+++ b/js/src/config/rules.mk
@@ -724,17 +724,19 @@ HOST_LIBS_DEPS = $(filter %.$(LIB_SUFFIX
 
 # Dependencies which, if modified, should cause everything to rebuild
 GLOBAL_DEPS += Makefile $(DEPTH)/config/autoconf.mk $(topsrcdir)/config/config.mk
 ifndef NO_MAKEFILE_RULE
 GLOBAL_DEPS += Makefile.in
 endif
 
 ##############################################
-compile:: $(OBJS) $(HOST_OBJS)
+OBJ_TARGETS = $(OBJS) $(PROGOBJS) $(HOST_OBJS) $(HOST_PROGOBJS)
+
+compile:: $(OBJ_TARGETS)
 
 include $(topsrcdir)/config/makefiles/target_libs.mk
 
 ifdef IS_TOOL_DIR
 # One would think "tools:: libs" would work, but it turns out that combined with
 # bug 907365, this makes make forget to run some rules sometimes.
 tools::
 	@$(MAKE) libs
--- a/python/mozbuild/mozbuild/backend/recursivemake.py
+++ b/python/mozbuild/mozbuild/backend/recursivemake.py
@@ -427,20 +427,21 @@ class RecursiveMakeBackend(CommonBackend
             return current, subdirs.parallel, \
                 subdirs.dirs + subdirs.tests + subdirs.tools
 
         # Skip tools dirs during libs traversal
         def libs_filter(current, subdirs):
             return current, subdirs.parallel, \
                 subdirs.static + subdirs.dirs + subdirs.tests
 
-        # compile and tools tiers use the same traversal as export
+        # compile, binaries and tools tiers use the same traversal as export
         filters = {
             'export': export_filter,
             'compile': parallel_filter,
+            'binaries': parallel_filter,
             'libs': libs_filter,
             'tools': parallel_filter,
         }
 
         root_deps_mk = Makefile()
 
         # Fill the dependencies for traversal of each tier.
         for tier, filter in filters.items():