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 163398 6be8c784235c92a7c72c1cf9cc797cf564cc8335
parent 163397 99e29833c316db196c00b250e654256e8b9dd19b
child 163399 c5906eed61fcc7a83f8ed23cdbbc868d6c45aaaf
push idunknown
push userunknown
push dateunknown
reviewersgps
bugs905973
milestone27.0a1
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():