Bug 928195 - Part 4: Rewrite WebIDL build system integration; r=bz, r=glandium
authorGregory Szorc <gps@mozilla.com>
Thu, 12 Dec 2013 16:26:38 +0900
changeset 176371 67fa1478308ebe2874cf965cd240d8aebf0ebde7
parent 176370 f9614eb176adce6995f4c26afc125e804ba065c2
child 176372 0d9f8da0806ee18af8561672dc2998821099e246
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, glandium
bugs928195
milestone29.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 928195 - Part 4: Rewrite WebIDL build system integration; r=bz, r=glandium WebIDL build system integration has been rewritten from the ground up. Changes: * GlobalGen.py, BindingGen.py, and ExampleGen.py have been removed in favor of mozwebidl.py. * Static .webidl files are now processed directly in their original location and aren't copied to the object directory. * Generated events <stem>.cpp files are now compiled into the unified sources. Previously, only the <stem>Binding.cpp files were compiled into unified sources. * Exported .h files are now generated directly into their final location. Previously, they were generated into the local directory then installed in their final location. * The list of globalgen-generated files now lives in Python and isn't duplicated in 3 places. * The make dependencies are much simpler as a result of using a single command to perform all code generation. The auto-generated .pp file from code generation sets up all dependencies necessary to reinvoke code generation and Python takes care of dependency management.
CLOBBER
dom/bindings/BindingGen.py
dom/bindings/Codegen.py
dom/bindings/ExampleGen.py
dom/bindings/GlobalGen.py
dom/bindings/Makefile.in
dom/bindings/moz.build
dom/bindings/mozwebidlcodegen/__init__.py
dom/bindings/test/Makefile.in
dom/bindings/test/moz.build
dom/moz.build
dom/webidl/moz.build
python/mozbuild/mozbuild/action/webidl.py
python/mozbuild/mozbuild/backend/common.py
python/mozbuild/mozbuild/backend/recursivemake.py
python/mozbuild/mozbuild/base.py
--- a/CLOBBER
+++ b/CLOBBER
@@ -13,9 +13,14 @@
 #          |               |
 #          O <-- Clobber   O  <-- Clobber
 #
 # Note: The description below will be part of the error message shown to users.
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
-Bug 887836 - webidl changes require a Windows clobber.
+# Are you updating CLOBBER because you think it's needed for your WebIDL
+# changes to stick? As of bug 928195, this shouldn't be necessary! Please
+# don't change CLOBBER for WebIDL changes any more.
+Bug 928195 rewrote WebIDL build system integration from the ground up. This
+will hopefully be the last required clobber due to WebIDLs poorly interacting
+with the build system.
deleted file mode 100644
--- a/dom/bindings/BindingGen.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
-# You can obtain one at http://mozilla.org/MPL/2.0/.
-
-import os
-import cPickle
-from Configuration import Configuration
-from Codegen import CGBindingRoot, replaceFileIfChanged, CGEventRoot
-from mozbuild.makeutil import Makefile
-from mozbuild.pythonutil import iter_modules_in_path
-from buildconfig import topsrcdir
-
-
-def generate_binding_files(config, outputprefix, srcprefix, webidlfile,
-                           generatedEventsWebIDLFiles):
-    """
-    |config| Is the configuration object.
-    |outputprefix| is a prefix to use for the header guards and filename.
-    """
-
-    depsname = ".deps/" + outputprefix + ".pp"
-    root = CGBindingRoot(config, outputprefix, webidlfile)
-    replaceFileIfChanged(outputprefix + ".h", root.declare())
-    replaceFileIfChanged(outputprefix + ".cpp", root.define())
-
-    if webidlfile in generatedEventsWebIDLFiles:
-        eventName = webidlfile[:-len(".webidl")]
-        generatedEvent = CGEventRoot(config, eventName)
-        replaceFileIfChanged(eventName + ".h", generatedEvent.declare())
-        replaceFileIfChanged(eventName + ".cpp", generatedEvent.define())
-
-    mk = Makefile()
-    # NOTE: it's VERY important that we output dependencies for the FooBinding
-    # file here, not for the header or generated cpp file.  These dependencies
-    # are used later to properly determine changedDeps and prevent rebuilding
-    # too much.  See the comment explaining $(binding_dependency_trackers) in
-    # Makefile.in.
-    rule = mk.create_rule([outputprefix])
-    rule.add_dependencies(os.path.join(srcprefix, x) for x in sorted(root.deps()))
-    rule.add_dependencies(iter_modules_in_path(topsrcdir))
-    with open(depsname, 'w') as f:
-        mk.dump(f)
-
-def main():
-    # Parse arguments.
-    from optparse import OptionParser
-    usagestring = "usage: %prog [header|cpp] configFile outputPrefix srcPrefix webIDLFile"
-    o = OptionParser(usage=usagestring)
-    o.add_option("--verbose-errors", action='store_true', default=False,
-                 help="When an error happens, display the Python traceback.")
-    (options, args) = o.parse_args()
-
-    configFile = os.path.normpath(args[0])
-    srcPrefix = os.path.normpath(args[1])
-
-    # Load the configuration
-    f = open('ParserResults.pkl', 'rb')
-    config = cPickle.load(f)
-    f.close()
-
-    def readFile(f):
-        file = open(f, 'rb')
-        try:
-            contents = file.read()
-        finally:
-            file.close()
-        return contents
-    allWebIDLFiles = readFile(args[2]).split()
-    generatedEventsWebIDLFiles = readFile(args[3]).split()
-    changedDeps = readFile(args[4]).split()
-
-    if all(f.endswith("Binding") or f == "ParserResults.pkl" for f in changedDeps):
-        toRegenerate = filter(lambda f: f.endswith("Binding"), changedDeps)
-        if len(toRegenerate) == 0 and len(changedDeps) == 1:
-            # Work around build system bug 874923: if we get here that means
-            # that changedDeps contained only one entry and it was
-            # "ParserResults.pkl".  That should never happen: if the
-            # ParserResults.pkl changes then either one of the globalgen files
-            # changed (in which case we wouldn't be in this "only
-            # ParserResults.pkl and *Binding changed" code) or some .webidl
-            # files changed (and then the corresponding *Binding files should
-            # show up in changedDeps).  Since clearly the build system is
-            # confused, just regenerate everything to be safe.
-            toRegenerate = allWebIDLFiles
-        else:
-            toRegenerate = map(lambda f: f[:-len("Binding")] + ".webidl",
-                               toRegenerate)
-    else:
-        toRegenerate = allWebIDLFiles
-
-    for webIDLFile in toRegenerate:
-        assert webIDLFile.endswith(".webidl")
-        outputPrefix = webIDLFile[:-len(".webidl")] + "Binding"
-        generate_binding_files(config, outputPrefix, srcPrefix, webIDLFile,
-                               generatedEventsWebIDLFiles);
-
-if __name__ == '__main__':
-    main()
--- a/dom/bindings/Codegen.py
+++ b/dom/bindings/Codegen.py
@@ -24,37 +24,16 @@ HASINSTANCE_HOOK_NAME = '_hasInstance'
 NEWRESOLVE_HOOK_NAME = '_newResolve'
 ENUMERATE_HOOK_NAME= '_enumerate'
 ENUM_ENTRY_VARIABLE_NAME = 'strings'
 INSTANCE_RESERVED_SLOTS = 3
 
 def memberReservedSlot(member):
     return "(DOM_INSTANCE_RESERVED_SLOTS + %d)" % member.slotIndex
 
-def replaceFileIfChanged(filename, newContents):
-    """
-    Read a copy of the old file, so that we don't touch it if it hasn't changed.
-    Returns True if the file was updated, false otherwise.
-    """
-    oldFileContents = ""
-    try:
-        oldFile = open(filename, 'rb')
-        oldFileContents = ''.join(oldFile.readlines())
-        oldFile.close()
-    except:
-        pass
-
-    if newContents == oldFileContents:
-        return False
-
-    f = open(filename, 'wb')
-    f.write(newContents)
-    f.close()
-    return True
-
 def toStringBool(arg):
     return str(not not arg).lower()
 
 def toBindingNamespace(arg):
     return re.sub("((_workers)?$)", "Binding\\1", arg);
 
 def isTypeCopyConstructible(type):
     # Nullable and sequence stuff doesn't affect copy/constructibility
deleted file mode 100644
--- a/dom/bindings/ExampleGen.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
-# You can obtain one at http://mozilla.org/MPL/2.0/.
-
-import os
-import cPickle
-from Configuration import Configuration
-from Codegen import CGExampleRoot, replaceFileIfChanged
-
-def generate_interface_example(config, interfaceName):
-    """
-    |config| Is the configuration object.
-    |interfaceName| is the name of the interface we're generating an example for.
-    """
-
-    root = CGExampleRoot(config, interfaceName)
-    exampleHeader = interfaceName + "-example.h"
-    exampleImpl = interfaceName + "-example.cpp"
-    replaceFileIfChanged(exampleHeader, root.declare())
-    replaceFileIfChanged(exampleImpl, root.define())
-
-def main():
-
-    # Parse arguments.
-    from optparse import OptionParser
-    usagestring = "usage: %prog configFile interfaceName"
-    o = OptionParser(usage=usagestring)
-    o.add_option("--verbose-errors", action='store_true', default=False,
-                 help="When an error happens, display the Python traceback.")
-    (options, args) = o.parse_args()
-
-    if len(args) != 2:
-        o.error(usagestring)
-    configFile = os.path.normpath(args[0])
-    interfaceName = args[1]
-
-    # Load the configuration
-    f = open('ParserResults.pkl', 'rb')
-    config = cPickle.load(f)
-    f.close()
-
-    # Generate the example class.
-    generate_interface_example(config, interfaceName)
-
-if __name__ == '__main__':
-    main()
deleted file mode 100644
--- a/dom/bindings/GlobalGen.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# 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/.
-
-# We do one global pass over all the WebIDL to generate our prototype enum
-# and generate information for subsequent phases.
-
-import os
-import WebIDL
-import cPickle
-from Configuration import Configuration
-from Codegen import GlobalGenRoots, replaceFileIfChanged
-
-def generate_file(config, name, action):
-
-    root = getattr(GlobalGenRoots, name)(config)
-    if action is 'declare':
-        filename = name + '.h'
-        code = root.declare()
-    else:
-        assert action is 'define'
-        filename = name + '.cpp'
-        code = root.define()
-
-    if replaceFileIfChanged(filename, code):
-        print "Generating %s" % (filename)
-    else:
-        print "%s hasn't changed - not touching it" % (filename)
-
-def main():
-    # Parse arguments.
-    from optparse import OptionParser
-    usageString = "usage: %prog [options] webidldir [files]"
-    o = OptionParser(usage=usageString)
-    o.add_option("--cachedir", dest='cachedir', default=None,
-                 help="Directory in which to cache lex/parse tables.")
-    o.add_option("--verbose-errors", action='store_true', default=False,
-                 help="When an error happens, display the Python traceback.")
-    (options, args) = o.parse_args()
-
-    if len(args) < 2:
-        o.error(usageString)
-
-    configFile = args[0]
-    baseDir = args[1]
-    fileList = args[2:]
-
-    # Parse the WebIDL.
-    parser = WebIDL.Parser(options.cachedir)
-    for filename in fileList:
-        fullPath = os.path.normpath(os.path.join(baseDir, filename))
-        f = open(fullPath, 'rb')
-        lines = f.readlines()
-        f.close()
-        parser.parse(''.join(lines), fullPath)
-    parserResults = parser.finish()
-
-    # Load the configuration.
-    config = Configuration(configFile, parserResults)
-
-    # Write the configuration out to a pickle.
-    resultsFile = open('ParserResults.pkl', 'wb')
-    cPickle.dump(config, resultsFile, -1)
-    resultsFile.close()
-
-    # Generate the atom list.
-    generate_file(config, 'GeneratedAtomList', 'declare')
-
-    # Generate the prototype list.
-    generate_file(config, 'PrototypeList', 'declare')
-
-    # Generate the common code.
-    generate_file(config, 'RegisterBindings', 'declare')
-    generate_file(config, 'RegisterBindings', 'define')
-
-    generate_file(config, 'UnionTypes', 'declare')
-    generate_file(config, 'UnionTypes', 'define')
-    generate_file(config, 'UnionConversions', 'declare')
-
-if __name__ == '__main__':
-    main()
--- a/dom/bindings/Makefile.in
+++ b/dom/bindings/Makefile.in
@@ -1,245 +1,85 @@
 # 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/.
+# 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/.
 
-webidl_base = $(topsrcdir)/dom/webidl
+abs_dist := $(abspath $(DIST))
+webidl_base := $(topsrcdir)/dom/webidl
+
 # Generated by moz.build
 include webidlsrcs.mk
 
-binding_include_path := mozilla/dom
-webidl_files += $(generated_events_webidl_files)
-all_webidl_files = $(webidl_files) $(generated_webidl_files) $(preprocessed_webidl_files)
-
-# Set exported_binding_headers before adding the test IDL to the mix
-exported_binding_headers := $(subst .webidl,Binding.h,$(all_webidl_files))
-exported_generated_events_headers := $(subst .webidl,.h,$(generated_events_webidl_files))
-
-# Set linked_binding_cpp_files before adding the test IDL to the mix
-linked_binding_cpp_files := $(subst .webidl,Binding.cpp,$(all_webidl_files))
-linked_generated_events_cpp_files := $(subst .webidl,.cpp,$(generated_events_webidl_files))
-
-all_webidl_files += $(test_webidl_files) $(preprocessed_test_webidl_files)
-
-generated_header_files := $(subst .webidl,Binding.h,$(all_webidl_files)) $(exported_generated_events_headers)
-generated_cpp_files := $(subst .webidl,Binding.cpp,$(all_webidl_files)) $(linked_generated_events_cpp_files)
-
-# We want to be able to only regenerate the .cpp and .h files that really need
-# to change when a .webidl file changes.  We do this by making the
-# binding_dependency_trackers targets have dependencies on the right .webidl
-# files via generated .pp files, having a .BindingGen target that depends on the
-# binding_dependency_trackers and which has all the generated binding .h/.cpp
-# depending on it, and then in the make commands for that target being able to
-# check which exact binding_dependency_trackers changed.
-binding_dependency_trackers := $(subst .webidl,Binding,$(all_webidl_files))
-
-globalgen_targets := \
-  GeneratedAtomList.h \
-  PrototypeList.h \
-  RegisterBindings.h \
-  RegisterBindings.cpp \
-  UnionTypes.h \
-  UnionTypes.cpp \
-  UnionConversions.h \
-  $(NULL)
-
-# Nasty hack: when the test/Makefile.in invokes us to do codegen, it
-# uses a target of
-# "export TestExampleInterface-example TestExampleProxyInterface-example".
-# We don't actually need to load our .o.pp files in that case, so just
-# pretend like we have no CPPSRCS if that's the target.  It makes the
-# test cycle much faster, which is why we're doing it.
-#
-# XXXbz We could try to cheat even more and only include our CPPSRCS
-# when $(MAKECMDGOALS) contains libs, so that we can skip loading all
-# those .o.pp when trying to make a single .cpp file too, but that
-# would break |make FooBinding.o(bj)|.  Ah, well.
-ifneq (export TestExampleInterface-example TestExampleProxyInterface-example,$(MAKECMDGOALS))
-CPPSRCS = \
-  $(unified_binding_cpp_files) \
-  $(linked_generated_events_cpp_files) \
-  $(filter %.cpp, $(globalgen_targets)) \
-  $(NULL)
+ifdef GNU_CC
+OS_CXXFLAGS += -Wno-uninitialized
 endif
 
-ABS_DIST := $(abspath $(DIST))
-
-EXTRA_EXPORT_MDDEPEND_FILES := $(addsuffix .pp,$(binding_dependency_trackers))
-
-EXPORTS_GENERATED_FILES := $(exported_binding_headers) $(exported_generated_events_headers)
-EXPORTS_GENERATED_DEST := $(ABS_DIST)/include/$(binding_include_path)
-EXPORTS_GENERATED_TARGET := export
-INSTALL_TARGETS += EXPORTS_GENERATED
+# These come from webidlsrcs.mk.
+# TODO Write directly into backend.mk.
+CPPSRCS += $(globalgen_sources) $(unified_binding_cpp_files)
 
-# Install auto-generated GlobalGen files. The rules for the install must
-# be in the same target/subtier as GlobalGen.py, otherwise the files will not
-# get installed into the appropriate location as they are generated.
-globalgen_headers_FILES := \
-  GeneratedAtomList.h \
-  PrototypeList.h \
-  RegisterBindings.h \
-  UnionConversions.h \
-  UnionTypes.h \
-  $(NULL)
-globalgen_headers_DEST = $(ABS_DIST)/include/mozilla/dom
-globalgen_headers_TARGET := export
-INSTALL_TARGETS += globalgen_headers
+# Generated bindings reference *Binding.h, not mozilla/dom/*Binding.h. And,
+# since we generate exported bindings directly to $(DIST)/include, we need
+# to add that path to the search list.
+#
+# Ideally, binding generation uses the prefixed header file names.
+# Bug 932092 tracks.
+LOCAL_INCLUDES += -I$(DIST)/include/mozilla/dom
 
 PYTHON_UNIT_TESTS += $(srcdir)/mozwebidlcodegen/test/test_mozwebidlcodegen.py
 
 include $(topsrcdir)/config/rules.mk
 
-ifdef GNU_CC
-CXXFLAGS += -Wno-uninitialized
-endif
-
-# If you change bindinggen_dependencies here, change it in
-# dom/bindings/test/Makefile.in too.
-bindinggen_dependencies := \
-  BindingGen.py \
-  Bindings.conf \
-  Configuration.py \
-  Codegen.py \
-  ParserResults.pkl \
-  parser/WebIDL.py \
+# TODO This list should be emitted to a .pp file via
+# GenerateCSS2PropertiesWebIDL.py.
+css2properties_dependencies = \
+  $(topsrcdir)/layout/style/nsCSSPropList.h \
+  $(topsrcdir)/layout/style/nsCSSPropAliasList.h \
+  $(webidl_base)/CSS2Properties.webidl.in \
+  $(webidl_base)/CSS2PropertiesProps.h \
+  $(srcdir)/GenerateCSS2PropertiesWebIDL.py \
   $(GLOBAL_DEPS) \
   $(NULL)
 
-CSS2Properties.webidl: $(topsrcdir)/layout/style/nsCSSPropList.h \
-                       $(topsrcdir)/layout/style/nsCSSPropAliasList.h \
-                       $(webidl_base)/CSS2Properties.webidl.in \
-                       $(webidl_base)/CSS2PropertiesProps.h \
-                       $(srcdir)/GenerateCSS2PropertiesWebIDL.py \
-                       $(GLOBAL_DEPS)
-	$(CPP) $(DEFINES) $(ACDEFINES) -I$(topsrcdir)/layout/style $(webidl_base)/CSS2PropertiesProps.h | \
-	  PYTHONDONTWRITEBYTECODE=1 $(PYTHON) \
-	  $(srcdir)/GenerateCSS2PropertiesWebIDL.py $(webidl_base)/CSS2Properties.webidl.in > CSS2Properties.webidl
-
-$(webidl_files): %: $(webidl_base)/%
-	$(INSTALL) $(IFLAGS1) $(webidl_base)/$* .
-
-$(test_webidl_files): %: $(srcdir)/test/%
-	$(INSTALL) $(IFLAGS1) $(srcdir)/test/$* .
-
-# We can't easily use PP_TARGETS here because it insists on outputting targets
-# that look like "$(CURDIR)/foo" whereas we want our target to just be "foo".
-# Make sure to include $(GLOBAL_DEPS) so we pick up changes to what symbols are
-# defined.  Also make sure to remove $@ before writing to it, because otherwise
-# if a file goes from non-preprocessed to preprocessed we can end up writing to
-# a symlink, which will clobber files in the srcdir, which is bad.
-$(preprocessed_webidl_files): %: $(webidl_base)/% $(GLOBAL_DEPS)
-	$(RM) $@
-	$(call py_action,preprocessor, \
-          $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $(webidl_base)/$* -o $@)
-
-# See the comment about PP_TARGETS for $(preprocessed_webidl_files)
-$(preprocessed_test_webidl_files): %: $(srcdir)/test/% $(GLOBAL_DEPS)
-	$(RM) $@
-	$(call py_action,preprocessor, \
-          $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $(srcdir)/test/$* -o $@)
+CSS2Properties.webidl: $(css2properties_dependencies)
+	$(CPP) $(DEFINES) $(ACDEFINES) -I$(topsrcdir)/layout/style \
+	  $(webidl_base)/CSS2PropertiesProps.h | \
+	    PYTHONDONTWRITEBYTECODE=1 $(PYTHON) \
+	      $(srcdir)/GenerateCSS2PropertiesWebIDL.py \
+	      $(webidl_base)/CSS2Properties.webidl.in > $@
 
-# Make is dumb and can get confused between "foo" and "$(CURDIR)/foo".  Make
-# sure that the latter depends on the former, since the latter gets used in .pp
-# files.
-all_webidl_files_absolute = $(addprefix $(CURDIR)/,$(all_webidl_files))
-$(all_webidl_files_absolute): $(CURDIR)/%: %
-
-$(generated_header_files): .BindingGen
-
-$(generated_cpp_files): .BindingGen
-
-# $(binding_dependency_trackers) pick up additional dependencies via .pp files
-# The rule: just brings the tracker up to date, if it's out of date, so that
-# we'll know that we have to redo binding generation and flag this prerequisite
-# there as being newer than the bindinggen target.
-$(binding_dependency_trackers):
-	@$(TOUCH) $@
-
-$(globalgen_targets): ParserResults.pkl
-
-%-example: .BindingGen
-	PYTHONDONTWRITEBYTECODE=1 $(PYTHON) $(topsrcdir)/config/pythonpath.py \
-	  $(PLY_INCLUDE) -I$(srcdir)/parser \
-	  $(srcdir)/ExampleGen.py \
-	  $(srcdir)/Bindings.conf $*
-
-CACHE_DIR = _cache
-
-globalgen_dependencies := \
-  GlobalGen.py \
-  Bindings.conf \
-  Configuration.py \
-  Codegen.py \
-  parser/WebIDL.py \
-  webidlsrcs.mk \
-  $(all_webidl_files) \
-  $(CACHE_DIR)/.done \
+# Most of the logic for dependencies lives inside Python so it can be
+# used by multiple build backends. We simply have rules to generate
+# and include the .pp file.
+#
+# The generated .pp file contains all the important dependencies such as
+# changes to .webidl or .py files should result in code generation being
+# performed.
+codegen_dependencies := \
+  $(nonstatic_webidl_files) \
   $(GLOBAL_DEPS) \
   $(NULL)
 
-$(CACHE_DIR)/.done:
-	$(MKDIR) -p $(CACHE_DIR)
+$(call include_deps,codegen.pp)
+
+codegen.pp: $(codegen_dependencies)
+	$(call py_action,webidl,$(srcdir))
 	@$(TOUCH) $@
 
-# Running GlobalGen.py updates ParserResults.pkl as a side-effect
-ParserResults.pkl: $(globalgen_dependencies)
-	$(info Generating global WebIDL files)
-	PYTHONDONTWRITEBYTECODE=1 $(PYTHON) $(topsrcdir)/config/pythonpath.py \
-	  $(PLY_INCLUDE) -I$(srcdir)/parser \
-	  $(srcdir)/GlobalGen.py $(srcdir)/Bindings.conf . \
-	  --cachedir=$(CACHE_DIR) \
-	  $(all_webidl_files)
-
-$(globalgen_headers_FILES): ParserResults.pkl
-
-# Make sure .deps actually exists, since we'll try to write to it from
-# BindingGen.py but we're typically running in the export phase, which is
-# before anyone has bothered creating .deps.
-# Then, pass our long lists through files to try to avoid blowing out the
-# command line.
-# Next, BindingGen.py will examine the changed dependency list to figure out
-# what it really needs to regenerate.
-# Finally, touch the .BindingGen file so that we don't have to keep redoing
-# all that until something else actually changes.
-.BindingGen: $(bindinggen_dependencies) $(binding_dependency_trackers)
-	$(info Generating WebIDL bindings)
-	$(MKDIR) -p .deps
-	echo $(all_webidl_files) > .all-webidl-file-list
-	echo $(generated_events_webidl_files) > .generated-events-webidl-files
-	echo $? > .changed-dependency-list
-	PYTHONDONTWRITEBYTECODE=1 $(PYTHON) $(topsrcdir)/config/pythonpath.py \
-	  $(PLY_INCLUDE) -I$(srcdir)/parser \
-	  $(srcdir)/BindingGen.py \
-	  $(srcdir)/Bindings.conf \
-	  $(CURDIR) \
-	  .all-webidl-file-list \
-	  .generated-events-webidl-files \
-	  .changed-dependency-list
-	@$(TOUCH) $@
+.PHONY: compiletests
+compiletests:
+	$(call SUBMAKE,libs,test)
 
 GARBAGE += \
-  webidlyacc.py \
+  codegen.pp \
+  codegen.json \
   parser.out \
-  $(wildcard *-example.h) \
-  $(wildcard *-example.cpp) \
-  .BindingGen \
-  .all-webidl-file-list \
-  .generated-events-webidl-files \
-  .changed-dependency-list \
-  $(binding_dependency_trackers) \
+  WebIDLGrammar.pkl \
+  $(wildcard *.h) \
+  $(wildcard *Binding.cpp) \
+  $(wildcard *Event.cpp) \
+  $(wildcard *-event.cpp) \
+  $(wildcard *.webidl) \
   $(NULL)
 
-# Make sure all binding header files are created during the export stage, so we
-# don't have issues with .cpp files being compiled before we've generated the
-# headers they depend on.  This is really only needed for the test files, since
-# the non-test headers are all exported above anyway.  Note that this means that
-# we do all of our codegen during export.
-export:: $(generated_header_files)
-
-distclean::
-	-$(RM) \
-        $(generated_header_files) \
-        $(generated_cpp_files) \
-        $(all_webidl_files) \
-        $(globalgen_targets) \
-        ParserResults.pkl \
-        $(NULL)
+DIST_GARBAGE += \
+  file-lists.json \
+  $(NULL)
--- a/dom/bindings/moz.build
+++ b/dom/bindings/moz.build
@@ -1,14 +1,16 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
+TEST_DIRS += ['test']
+
 EXPORTS.mozilla += [
     'ErrorResult.h',
 ]
 
 EXPORTS.mozilla.dom += [
     'AtomList.h',
     'BindingDeclarations.h',
     'BindingUtils.h',
--- a/dom/bindings/mozwebidlcodegen/__init__.py
+++ b/dom/bindings/mozwebidlcodegen/__init__.py
@@ -12,16 +12,17 @@ import hashlib
 import json
 import logging
 import os
 
 from copy import deepcopy
 
 from mach.mixin.logging import LoggingMixin
 
+from mozbuild.base import MozbuildObject
 from mozbuild.makeutil import Makefile
 from mozbuild.pythonutil import iter_modules_in_path
 from mozbuild.util import FileAvoidWrite
 
 import mozpack.path as mozpath
 
 import WebIDL
 from Codegen import (
@@ -516,8 +517,49 @@ class WebIDLCodegenManager(LoggingMixin)
         existed, updated = fh.close()
 
         if not existed:
             result[0].add(path)
         elif updated:
             result[1].add(path)
         else:
             result[2].add(path)
+
+
+def create_build_system_manager(topsrcdir, topobjdir, dist_dir):
+    """Create a WebIDLCodegenManager for use by the build system."""
+    src_dir = os.path.join(topsrcdir, 'dom', 'bindings')
+    obj_dir = os.path.join(topobjdir, 'dom', 'bindings')
+
+    with open(os.path.join(obj_dir, 'file-lists.json'), 'rb') as fh:
+        files = json.load(fh)
+
+    inputs = (files['webidls'], files['exported_stems'],
+        files['generated_events_stems'])
+
+    cache_dir = os.path.join(obj_dir, '_cache')
+    try:
+        os.makedirs(cache_dir)
+    except OSError as e:
+        if e.errno != errno.EEXIST:
+            raise
+
+    return WebIDLCodegenManager(
+        os.path.join(src_dir, 'Bindings.conf'),
+        inputs,
+        os.path.join(dist_dir, 'include', 'mozilla', 'dom'),
+        obj_dir,
+        os.path.join(obj_dir, 'codegen.json'),
+        cache_dir=cache_dir,
+        # The make rules include a codegen.pp file containing dependencies.
+        make_deps_path=os.path.join(obj_dir, 'codegen.pp'),
+        make_deps_target='codegen.pp',
+    )
+
+
+class BuildSystemWebIDL(MozbuildObject):
+    @property
+    def manager(self):
+        if not hasattr(self, '_webidl_manager'):
+            self._webidl_manager = create_build_system_manager(
+                self.topsrcdir, self.topobjdir, self.distdir)
+
+        return self._webidl_manager
--- a/dom/bindings/test/Makefile.in
+++ b/dom/bindings/test/Makefile.in
@@ -1,90 +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/.
 
-# Do NOT export this library.  We don't actually want our test code
-# being added to libxul or anything.
-
-# pymake can't handle descending into dom/bindings several times simultaneously
-ifdef .PYMAKE
-.NOTPARALLEL:
-endif
-
-# Need this for $(test_webidl_files)
 include ../webidlsrcs.mk
 
-# But the webidl actually lives in our parent dir
-test_webidl_files := $(addprefix ../,$(test_webidl_files))
-# Store the actual locations of our source preprocessed files, so we
-# can depend on them sanely.
-source_preprocessed_test_webidl_files := $(addprefix $(srcdir)/,$(preprocessed_test_webidl_files))
-preprocessed_test_webidl_files := $(addprefix ../,$(preprocessed_test_webidl_files))
-
-CPPSRCS += \
-  $(subst .webidl,Binding.cpp,$(test_webidl_files)) \
-  $(subst .webidl,Binding.cpp,$(preprocessed_test_webidl_files)) \
-  $(NULL)
-
-# If you change bindinggen_dependencies here, change it in
-# dom/bindings/Makefile.in too.  But note that we include ../Makefile
-# here manually, since $(GLOBAL_DEPS) won't cover it.
-bindinggen_dependencies := \
-  ../BindingGen.py \
-  ../Bindings.conf \
-  ../Configuration.py \
-  ../Codegen.py \
-  ../ParserResults.pkl \
-  ../parser/WebIDL.py \
-  ../Makefile \
-  $(GLOBAL_DEPS) \
-  $(NULL)
+# $(test_sources) comes from webidlsrcs.mk.
+# TODO Update this variable in backend.mk.
+CPPSRCS += $(addprefix ../,$(test_sources))
 
 ifdef GNU_CC
-CXXFLAGS += -Wno-uninitialized
+OS_CXXFLAGS += -Wno-uninitialized
 endif
 
+# Bug 932082 tracks having bindings use namespaced includes.
+LOCAL_INCLUDES += -I$(DIST)/include/mozilla/dom -I..
+
 # Include rules.mk before any of our targets so our first target is coming from
 # rules.mk and running make with no target in this dir does the right thing.
 include $(topsrcdir)/config/rules.mk
 
-$(CPPSRCS): .BindingGen
-
-.BindingGen: $(bindinggen_dependencies) \
-             $(test_webidl_files) \
-             $(source_preprocessed_test_webidl_files) \
-             $(NULL)
-	# The export phase in dom/bindings is what actually looks at
-	# dependencies and regenerates things as needed, so just go ahead and
-	# make that phase here.  Also make our example interface files.  If the
-	# target used here ever changes, change the conditional around
-	# $(CPPSRCS) in dom/bindings/Makefile.in.
-	$(MAKE) -C .. export TestExampleInterface-example TestExampleProxyInterface-example
-	@$(TOUCH) $@
-
 check::
 	PYTHONDONTWRITEBYTECODE=1 $(PYTHON) $(topsrcdir)/config/pythonpath.py \
 	  $(PLY_INCLUDE) $(srcdir)/../parser/runtests.py
-
-# Since we define MOCHITEST_FILES, config/makefiles/mochitest.mk goes ahead and
-# sets up a rule with libs:: in itm which makes our .DEFAULT_TARGET be "libs".
-# Then ruls.mk does |.DEFAULT_TARGET ?= default| which leaves it as "libs".  So
-# if we make without an explicit target in this directory, we try to make
-# "libs", but with a $(MAKECMDGOALS) of empty string.  And then rules.mk
-# helpfully does not include our *.o.pp files, since it includes them only if
-# filtering some stuff out from $(MAKECMDGOALS) leaves it nonempty.  The upshot
-# is that if some headers change and we run make in this dir without an explicit
-# target things don't get rebuilt.
-#
-# On the other hand, if we set .DEFAULT_TARGET to "default" explicitly here,
-# then rules.mk will reinvoke make with "export" and "libs" but this time hey
-# will be passed as explicit targets, show up in $(MAKECMDGOALS), and things
-# will work.  Do this at the end of our Makefile so the rest of the build system
-# does not get a chance to muck with it after we set it.
-.DEFAULT_GOAL := default
-
-# Make sure to add .BindingGen to GARBAGE so we'll rebuild our example
-# files if someone goes through and deletes GARBAGE all over, which
-# will delete example files from our parent dir.
-GARBAGE += \
-  .BindingGen \
-  $(NULL)
--- a/dom/bindings/test/moz.build
+++ b/dom/bindings/test/moz.build
@@ -9,14 +9,25 @@ LIBXUL_LIBRARY = True
 # being added to libxul or anything.
 
 LIBRARY_NAME = 'dombindings_test_s'
 
 MOCHITEST_MANIFESTS += ['mochitest.ini']
 
 MOCHITEST_CHROME_MANIFESTS += ['chrome.ini']
 
+TEST_WEBIDL_FILES += [
+    'TestDictionary.webidl',
+    'TestJSImplInheritanceGen.webidl',
+    'TestTypedef.webidl',
+]
+
+PREPROCESSED_TEST_WEBIDL_FILES += [
+    'TestCodeGen.webidl',
+    'TestExampleGen.webidl',
+    'TestJSImplGen.webidl',
+]
+
 LOCAL_INCLUDES += [
     '/dom/bindings',
     '/js/xpconnect/src',
     '/js/xpconnect/wrappers',
 ]
-
--- a/dom/moz.build
+++ b/dom/moz.build
@@ -102,19 +102,16 @@ if CONFIG['MOZ_GAMEPAD']:
     PARALLEL_DIRS += ['gamepad']
 
 if CONFIG['MOZ_NFC']:
     PARALLEL_DIRS += ['nfc']
 
 if CONFIG['MOZ_B2G']:
     PARALLEL_DIRS += ['downloads']
 
-# bindings/test is here, because it needs to build after bindings/, and
-# we build subdirectories before ourselves.
 TEST_DIRS += [
     'tests',
     'imptests',
-    'bindings/test',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('gtk2', 'cocoa', 'windows', 'android', 'qt', 'os2'):
     TEST_DIRS += ['plugins/test']
 
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -548,28 +548,16 @@ if CONFIG['MOZ_WEBSPEECH']:
         'SpeechRecognitionEvent.webidl',
     ]
 
 if CONFIG['MOZ_B2G_FM']:
     WEBIDL_FILES += [
         'FMRadio.webidl',
     ]
 
-if CONFIG['ENABLE_TESTS']:
-    TEST_WEBIDL_FILES += [
-        'TestDictionary.webidl',
-        'TestJSImplInheritanceGen.webidl',
-        'TestTypedef.webidl',
-    ]
-    PREPROCESSED_TEST_WEBIDL_FILES += [
-        'TestCodeGen.webidl',
-        'TestExampleGen.webidl',
-        'TestJSImplGen.webidl',
-    ]
-
 GENERATED_EVENTS_WEBIDL_FILES = [
     'BlobEvent.webidl',
     'CallGroupErrorEvent.webidl',
     'DataStoreChangeEvent.webidl',
     'DeviceLightEvent.webidl',
     'DeviceProximityEvent.webidl',
     'DownloadEvent.webidl',
     'ErrorEvent.webidl',
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/action/webidl.py
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sys
+
+from mozwebidlcodegen import BuildSystemWebIDL
+
+
+def main(argv):
+    """Perform WebIDL code generation required by the build system."""
+    manager = BuildSystemWebIDL.from_environment().manager
+    manager.generate_build_files()
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
--- a/python/mozbuild/mozbuild/backend/common.py
+++ b/python/mozbuild/mozbuild/backend/common.py
@@ -5,25 +5,29 @@
 from __future__ import unicode_literals
 
 import json
 import os
 import re
 
 import mozpack.path as mozpath
 
-from mozbuild.preprocessor import Preprocessor
-
 from .base import BuildBackend
 
 from ..frontend.data import (
     ConfigFileSubstitution,
     HeaderFileSubstitution,
+    GeneratedEventWebIDLFile,
+    GeneratedWebIDLFile,
+    PreprocessedTestWebIDLFile,
+    PreprocessedWebIDLFile,
     TestManifest,
+    TestWebIDLFile,
     XPIDLFile,
+    WebIDLFile,
 )
 
 from ..util import DefaultOnReadDict
 
 
 class XPIDLManager(object):
     """Helps manage XPCOM IDLs in the context of the build system."""
     def __init__(self, config):
@@ -51,16 +55,89 @@ class XPIDLManager(object):
 
         if not allow_existing and entry['basename'] in self.idls:
             raise Exception('IDL already registered: %' % entry['basename'])
 
         self.idls[entry['basename']] = entry
         self.modules.setdefault(entry['module'], set()).add(entry['root'])
 
 
+class WebIDLCollection(object):
+    """Collects WebIDL info referenced during the build."""
+
+    def __init__(self):
+        self.sources = set()
+        self.generated_sources = set()
+        self.generated_events_sources = set()
+        self.preprocessed_sources = set()
+        self.test_sources = set()
+        self.preprocessed_test_sources = set()
+
+    def all_regular_sources(self):
+        return self.sources | self.generated_sources | \
+            self.generated_events_sources | self.preprocessed_sources
+
+    def all_regular_basenames(self):
+        return [os.path.basename(source) for source in self.all_regular_sources()]
+
+    def all_regular_stems(self):
+        return [os.path.splitext(b)[0] for b in self.all_regular_basenames()]
+
+    def all_regular_bindinggen_stems(self):
+        for stem in self.all_regular_stems():
+            yield '%sBinding' % stem
+
+        for source in self.generated_events_sources:
+            yield os.path.splitext(os.path.basename(source))[0]
+
+    def all_regular_cpp_basenames(self):
+        for stem in self.all_regular_bindinggen_stems():
+            yield '%s.cpp' % stem
+
+    def all_test_sources(self):
+        return self.test_sources | self.preprocessed_test_sources
+
+    def all_test_basenames(self):
+        return [os.path.basename(source) for source in self.all_test_sources()]
+
+    def all_test_stems(self):
+        return [os.path.splitext(b)[0] for b in self.all_test_basenames()]
+
+    def all_test_cpp_basenames(self):
+        return ['%sBinding.cpp' % s for s in self.all_test_stems()]
+
+    def all_static_sources(self):
+        return self.sources | self.generated_events_sources | \
+            self.test_sources
+
+    def all_non_static_sources(self):
+        return self.generated_sources | self.all_preprocessed_sources()
+
+    def all_non_static_basenames(self):
+        return [os.path.basename(s) for s in self.all_non_static_sources()]
+
+    def all_preprocessed_sources(self):
+        return self.preprocessed_sources | self.preprocessed_test_sources
+
+    def all_sources(self):
+        return set(self.all_regular_sources()) | set(self.all_test_sources())
+
+    def all_basenames(self):
+        return [os.path.basename(source) for source in self.all_sources()]
+
+    def all_stems(self):
+        return [os.path.splitext(b)[0] for b in self.all_basenames()]
+
+    def generated_events_basenames(self):
+        return [os.path.basename(s) for s in self.generated_events_sources]
+
+    def generated_events_stems(self):
+        return [os.path.splitext(b)[0] for b in self.generated_events_basenames()]
+
+
 class TestManager(object):
     """Helps hold state related to tests."""
 
     def __init__(self, config):
         self.config = config
         self.topsrcdir = mozpath.normpath(config.topsrcdir)
 
         self.tests_by_path = DefaultOnReadDict({}, global_default=[])
@@ -85,16 +162,17 @@ class TestManager(object):
 
 
 class CommonBackend(BuildBackend):
     """Holds logic common to all build backends."""
 
     def _init(self):
         self._idl_manager = XPIDLManager(self.environment)
         self._test_manager = TestManager(self.environment)
+        self._webidls = WebIDLCollection()
 
     def consume_object(self, obj):
         if isinstance(obj, TestManifest):
             for test in obj.tests:
                 self._test_manager.add(test, flavor=obj.flavor,
                     topsrcdir=obj.topsrcdir)
 
         elif isinstance(obj, XPIDLFile):
@@ -108,25 +186,51 @@ class CommonBackend(BuildBackend):
             with self._get_preprocessor(obj) as pp:
                 pp.do_include(obj.input_path)
             self.backend_input_files.add(obj.input_path)
 
         elif isinstance(obj, HeaderFileSubstitution):
             self._create_config_header(obj)
             self.backend_input_files.add(obj.input_path)
 
+        # We should consider aggregating WebIDL types in emitter.py.
+        elif isinstance(obj, WebIDLFile):
+            self._webidls.sources.add(mozpath.join(obj.srcdir, obj.basename))
+
+        elif isinstance(obj, GeneratedEventWebIDLFile):
+            self._webidls.generated_events_sources.add(mozpath.join(
+                obj.srcdir, obj.basename))
+
+        elif isinstance(obj, TestWebIDLFile):
+            self._webidls.test_sources.add(mozpath.join(obj.srcdir,
+                obj.basename))
+
+        elif isinstance(obj, PreprocessedTestWebIDLFile):
+            self._webidls.preprocessed_test_sources.add(mozpath.join(
+                obj.srcdir, obj.basename))
+
+        elif isinstance(obj, GeneratedWebIDLFile):
+            self._webidls.generated_sources.add(mozpath.join(obj.srcdir,
+                obj.basename))
+
+        elif isinstance(obj, PreprocessedWebIDLFile):
+            self._webidls.preprocessed_sources.add(mozpath.join(
+                obj.srcdir, obj.basename))
+
         else:
             return
 
         obj.ack()
 
     def consume_finished(self):
         if len(self._idl_manager.idls):
             self._handle_idl_manager(self._idl_manager)
 
+        self._handle_webidl_collection(self._webidls)
+
         # Write out a machine-readable file describing every test.
         path = mozpath.join(self.environment.topobjdir, 'all-tests.json')
         with self._write_file(path) as fh:
             json.dump(self._test_manager.tests_by_path, fh, sort_keys=True,
                 indent=2)
 
     def _create_config_header(self, obj):
         '''Creates the given config header. A config header is generated by
--- a/python/mozbuild/mozbuild/backend/recursivemake.py
+++ b/python/mozbuild/mozbuild/backend/recursivemake.py
@@ -1,56 +1,52 @@
 # 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/.
 
 from __future__ import unicode_literals
 
 import itertools
+import json
 import logging
 import os
-import re
 import types
 
 from collections import namedtuple
 
+import mozwebidlcodegen
+
 import mozbuild.makeutil as mozmakeutil
 from mozpack.copier import FilePurger
 from mozpack.manifests import (
     InstallManifest,
 )
 import mozpack.path as mozpath
 
 from .common import CommonBackend
 from ..frontend.data import (
     ConfigFileSubstitution,
     Defines,
     DirectoryTraversal,
     Exports,
-    GeneratedEventWebIDLFile,
     GeneratedInclude,
-    GeneratedWebIDLFile,
     HostProgram,
     HostSimpleProgram,
     InstallationTarget,
     IPDLFile,
     JavaJarData,
     LibraryDefinition,
     LocalInclude,
-    PreprocessedTestWebIDLFile,
-    PreprocessedWebIDLFile,
     Program,
     SandboxDerived,
     SandboxWrapped,
     SimpleProgram,
-    TestWebIDLFile,
+    TestManifest,
     VariablePassthru,
     XPIDLFile,
-    TestManifest,
-    WebIDLFile,
 )
 from ..util import (
     ensureParentDir,
     FileAvoidWrite,
 )
 from ..makeutil import Makefile
 
 class BackendMakeFile(object):
@@ -265,22 +261,16 @@ class RecursiveMakeBackend(CommonBackend
     recursive make and thus will need this backend.
     """
 
     def _init(self):
         CommonBackend._init(self)
 
         self._backend_files = {}
         self._ipdl_sources = set()
-        self._webidl_sources = set()
-        self._generated_events_webidl_sources = set()
-        self._test_webidl_sources = set()
-        self._preprocessed_test_webidl_sources = set()
-        self._preprocessed_webidl_sources = set()
-        self._generated_webidl_sources = set()
 
         def detailed(summary):
             s = '{:d} total backend files. {:d} created; {:d} updated; {:d} unchanged'.format(
                 summary.created_count + summary.updated_count +
                 summary.unchanged_count, summary.created_count,
                 summary.updated_count, summary.unchanged_count)
             if summary.deleted_count:
                 s+= '; {:d} deleted'.format(summary.deleted_count)
@@ -405,43 +395,16 @@ class RecursiveMakeBackend(CommonBackend
                 backend_file.write('\n')
 
         elif isinstance(obj, Exports):
             self._process_exports(obj, obj.exports, backend_file)
 
         elif isinstance(obj, IPDLFile):
             self._ipdl_sources.add(mozpath.join(obj.srcdir, obj.basename))
 
-        elif isinstance(obj, WebIDLFile):
-            self._webidl_sources.add(mozpath.join(obj.srcdir, obj.basename))
-            self._process_webidl_basename(obj.basename)
-
-        elif isinstance(obj, GeneratedEventWebIDLFile):
-            self._generated_events_webidl_sources.add(mozpath.join(obj.srcdir, obj.basename))
-
-        elif isinstance(obj, TestWebIDLFile):
-            self._test_webidl_sources.add(mozpath.join(obj.srcdir,
-                                                       obj.basename))
-            # Test WebIDL files are not exported.
-
-        elif isinstance(obj, PreprocessedTestWebIDLFile):
-            self._preprocessed_test_webidl_sources.add(mozpath.join(obj.srcdir,
-                                                                    obj.basename))
-            # Test WebIDL files are not exported.
-
-        elif isinstance(obj, GeneratedWebIDLFile):
-            self._generated_webidl_sources.add(mozpath.join(obj.srcdir,
-                                                            obj.basename))
-            self._process_webidl_basename(obj.basename)
-
-        elif isinstance(obj, PreprocessedWebIDLFile):
-            self._preprocessed_webidl_sources.add(mozpath.join(obj.srcdir,
-                                                               obj.basename))
-            self._process_webidl_basename(obj.basename)
-
         elif isinstance(obj, Program):
             self._process_program(obj.program, backend_file)
 
         elif isinstance(obj, HostProgram):
             self._process_host_program(obj.program, backend_file)
 
         elif isinstance(obj, SimpleProgram):
             self._process_simple_program(obj.program, backend_file)
@@ -602,16 +565,19 @@ class RecursiveMakeBackend(CommonBackend
                                  unified_prefix='Unified',
                                  unified_suffix='cpp',
                                  extra_dependencies=[],
                                  unified_files_makefile_variable='unified_files',
                                  include_curdir_build_rules=True,
                                  poison_windows_h=False,
                                  files_per_unified_file=16):
 
+        # In case it's a generator.
+        files = sorted(files)
+
         explanation = "\n" \
             "# We build files in 'unified' mode by including several files\n" \
             "# together into a single source file.  This cuts down on\n" \
             "# compilation times and debug information size.  %d was chosen as\n" \
             "# a reasonable compromise between clobber rebuild time, incremental\n" \
             "# rebuild time, and compiler memory usage." % files_per_unified_file
         makefile.add_statement(explanation)
 
@@ -627,17 +593,17 @@ class RecursiveMakeBackend(CommonBackend
 
             # From the itertools documentation, slightly modified:
             def grouper(n, iterable):
                 "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
                 args = [iter(iterable)] * n
                 return itertools.izip_longest(fillvalue=dummy_fill_value, *args)
 
             for i, unified_group in enumerate(grouper(files_per_unified_file,
-                                                      sorted(files))):
+                                                      files)):
                 just_the_filenames = list(filter_out_dummy(unified_group))
                 yield '%s%d.%s' % (unified_prefix, i, unified_suffix), just_the_filenames
 
         all_sources = ' '.join(source for source, _ in unified_files())
         makefile.add_statement('%s := %s' % (unified_files_makefile_variable,
                                                all_sources))
 
         for unified_file, source_filenames in unified_files():
@@ -745,52 +711,22 @@ class RecursiveMakeBackend(CommonBackend
                                       unified_files_makefile_variable='CPPSRCS')
 
         mk.add_statement('IPDLDIRS := %s' % ' '.join(sorted(set(mozpath.dirname(p)
             for p in self._ipdl_sources))))
 
         with self._write_file(mozpath.join(ipdl_dir, 'ipdlsrcs.mk')) as ipdls:
             mk.dump(ipdls, removal_guard=False)
 
-        self._may_skip['compile'] -= set(['ipc/ipdl'])
-
-        # Write out master lists of WebIDL source files.
-        bindings_dir = mozpath.join(self.environment.topobjdir, 'dom', 'bindings')
-
-        mk = mozmakeutil.Makefile()
-
-        def write_var(variable, sources):
-            files = [mozpath.basename(f) for f in sorted(sources)]
-            mk.add_statement('%s += %s' % (variable, ' '.join(files)))
-        write_var('webidl_files', self._webidl_sources)
-        write_var('generated_events_webidl_files', self._generated_events_webidl_sources)
-        write_var('test_webidl_files', self._test_webidl_sources)
-        write_var('preprocessed_test_webidl_files', self._preprocessed_test_webidl_sources)
-        write_var('generated_webidl_files', self._generated_webidl_sources)
-        write_var('preprocessed_webidl_files', self._preprocessed_webidl_sources)
-
-        all_webidl_files = itertools.chain(iter(self._webidl_sources),
-                                           iter(self._generated_events_webidl_sources),
-                                           iter(self._generated_webidl_sources),
-                                           iter(self._preprocessed_webidl_sources))
-        all_webidl_files = [mozpath.basename(x) for x in all_webidl_files]
-        all_webidl_sources = [re.sub(r'\.webidl$', 'Binding.cpp', x) for x in all_webidl_files]
-
-        self._add_unified_build_rules(mk, all_webidl_sources,
-                                      bindings_dir,
-                                      unified_prefix='UnifiedBindings',
-                                      unified_files_makefile_variable='unified_binding_cpp_files',
-                                      poison_windows_h=True)
-
-        # Assume that Somebody Else has responsibility for correctly
-        # specifying removal dependencies for |all_webidl_sources|.
-        with self._write_file(mozpath.join(bindings_dir, 'webidlsrcs.mk')) as webidls:
-            mk.dump(webidls, removal_guard=False)
-
-        self._may_skip['compile'] -= set(['dom/bindings', 'dom/bindings/test'])
+        # These contain autogenerated sources that the build config doesn't
+        # yet know about.
+        # TODO Emit GENERATED_SOURCES so these special cases are dealt with
+        # the proper way.
+        self._may_skip['compile'] -= {'ipc/ipdl'}
+        self._may_skip['compile'] -= {'dom/bindings', 'dom/bindings/test'}
 
         self._fill_root_mk()
 
         # Write out a dependency file used to determine whether a config.status
         # re-run is needed.
         inputs = sorted(p.replace(os.sep, '/') for p in self.backend_input_files)
 
         # We need to use $(DEPTH) so the target here matches what's in
@@ -1021,20 +957,16 @@ class RecursiveMakeBackend(CommonBackend
         backend_file.write('HOST_PROGRAM = %s\n' % program)
 
     def _process_simple_program(self, program, backend_file):
         backend_file.write('SIMPLE_PROGRAMS += %s\n' % program)
 
     def _process_host_simple_program(self, program, backend_file):
         backend_file.write('HOST_SIMPLE_PROGRAMS += %s\n' % program)
 
-    def _process_webidl_basename(self, basename):
-        header = 'mozilla/dom/%sBinding.h' % mozpath.splitext(basename)[0]
-        self._install_manifests['dist_include'].add_optional_exists(header)
-
     def _process_test_manifest(self, obj, backend_file):
         # Much of the logic in this function could be moved to CommonBackend.
         self.backend_input_files.add(mozpath.join(obj.topsrcdir,
             obj.manifest_relpath))
 
         # Duplicate manifests may define the same file. That's OK.
         for source, dest in obj.installs.items():
             try:
@@ -1166,8 +1098,91 @@ class RecursiveMakeBackend(CommonBackend
             pp.handleLine(b'include $(topsrcdir)/config/recurse.mk\n')
         if not stub:
             # Adding the Makefile.in here has the desired side-effect
             # that if the Makefile.in disappears, this will force
             # moz.build traversal. This means that when we remove empty
             # Makefile.in files, the old file will get replaced with
             # the autogenerated one automatically.
             self.backend_input_files.add(obj.input_path)
+
+    def _handle_webidl_collection(self, webidls):
+        if not webidls.all_stems():
+            return
+
+        bindings_dir = mozpath.join(self.environment.topobjdir, 'dom',
+            'bindings')
+
+        all_inputs = set(webidls.all_static_sources())
+        for s in webidls.all_non_static_basenames():
+            all_inputs.add(mozpath.join(bindings_dir, s))
+
+        generated_events_stems = webidls.generated_events_stems()
+        exported_stems = webidls.all_regular_stems()
+
+        # The WebIDL manager reads configuration from a JSON file. So, we
+        # need to write this file early.
+        o = dict(
+            webidls=sorted(all_inputs),
+            generated_events_stems=sorted(generated_events_stems),
+            exported_stems=sorted(exported_stems),
+        )
+
+        file_lists = mozpath.join(bindings_dir, 'file-lists.json')
+        with self._write_file(file_lists) as fh:
+            json.dump(o, fh, sort_keys=True, indent=2)
+
+        manager = mozwebidlcodegen.create_build_system_manager(
+            self.environment.topsrcdir,
+            self.environment.topobjdir,
+            mozpath.join(self.environment.topobjdir, 'dist')
+        )
+
+        # The manager is the source of truth on what files are generated.
+        # Consult it for install manifests.
+        include_dir = mozpath.join(self.environment.topobjdir, 'dist',
+            'include')
+        for f in manager.expected_build_output_files():
+            if f.startswith(include_dir):
+                self._install_manifests['dist_include'].add_optional_exists(
+                    mozpath.relpath(f, include_dir))
+
+        # We pass WebIDL info to make via a completely generated make file.
+        mk = Makefile()
+        mk.add_statement('nonstatic_webidl_files := %s' % ' '.join(
+            sorted(webidls.all_non_static_basenames())))
+        mk.add_statement('globalgen_sources := %s' % ' '.join(
+            sorted(manager.GLOBAL_DEFINE_FILES)))
+        mk.add_statement('test_sources := %s' % ' '.join(
+            sorted('%sBinding.cpp' % s for s in webidls.all_test_stems())))
+
+        # Add rules to preprocess bindings.
+        # This should ideally be using PP_TARGETS. However, since the input
+        # filenames match the output filenames, the existing PP_TARGETS rules
+        # result in circular dependencies and other make weirdness. One
+        # solution is to rename the input or output files repsectively. See
+        # bug 928195 comment 129.
+        for source in sorted(webidls.all_preprocessed_sources()):
+            basename = os.path.basename(source)
+            rule = mk.create_rule([basename])
+            rule.add_dependencies([source, '$(GLOBAL_DEPS)'])
+            rule.add_commands([
+                # Remove the file before writing so bindings that go from
+                # static to preprocessed don't end up writing to a symlink,
+                # which would modify content in the source directory.
+                '$(RM) $@',
+                '$(call py_action,preprocessor,$(DEFINES) $(ACDEFINES) '
+                    '$(XULPPFLAGS) $< -o $@)'
+            ])
+
+        # Bindings are compiled in unified mode to speed up compilation and
+        # to reduce linker memory size. Note that test bindings are separated
+        # from regular ones so tests bindings aren't shipped.
+        self._add_unified_build_rules(mk,
+            webidls.all_regular_cpp_basenames(),
+            bindings_dir,
+            unified_prefix='UnifiedBindings',
+            unified_files_makefile_variable='unified_binding_cpp_files',
+            poison_windows_h=True)
+
+        webidls_mk = mozpath.join(bindings_dir, 'webidlsrcs.mk')
+        with self._write_file(webidls_mk) as fh:
+            mk.dump(fh, removal_guard=False)
--- a/python/mozbuild/mozbuild/base.py
+++ b/python/mozbuild/mozbuild/base.py
@@ -269,16 +269,20 @@ class MozbuildObject(ProcessExecutionMix
     def distdir(self):
         return os.path.join(self.topobjdir, 'dist')
 
     @property
     def bindir(self):
         return os.path.join(self.topobjdir, 'dist', 'bin')
 
     @property
+    def includedir(self):
+        return os.path.join(self.topobjdir, 'dist', 'include')
+
+    @property
     def statedir(self):
         return os.path.join(self.topobjdir, '.mozbuild')
 
     def remove_objdir(self):
         """Remove the entire object directory."""
 
         # We use mozfile because it is faster than shutil.rmtree().
         # mozfile doesn't like unicode arguments (bug 818783).