Bug 1336429 - Add the ability to build GN projects in the tree with mozbuild. draft
authorChris Manchester <cmanchester@mozilla.com>
Thu, 07 Dec 2017 15:58:18 -0800
changeset 711810 23bd292001381e642b195d10aa5004cf27d269cd
parent 711809 2386800ec051598ff4dd42da1118abcf05299fc1
child 743883 eb16b085069575c9e198b0cccb32a9c397594a38
push id93153
push userbmo:cmanchester@mozilla.com
push dateThu, 14 Dec 2017 19:36:43 +0000
bugs1336429
milestone59.0a1
Bug 1336429 - Add the ability to build GN projects in the tree with mozbuild. This commit adds a frontend construct, `GN_DIRS`, to facilitate building gn projects with moz.build. Directories added to `GN_DIRS` get particular treatment by two build backends added here as well, `GnConfigGen` and `GnMozbuildWriter`. The `GnConfigGen` backend runs `gn gen` for a gn project specified in `GN_DIRS` and dumps this configuration as json, which is filtered to include only those elements that will be needed by mozbuild. `gn gen` is run in the context of a single build's configuration, so when adding or updating a gn project it will be necessary to run this step with each supported configuration. The `GnMozbuildWriter` aggregates the config files generated by the `GnConfigGen` backend, which it expects to find in the `gn-configs` directory under the directory specified to `GN_DIRS`. The result is written to a set of moz.build files suitable for building the project that are intended to be checked in to the tree. Once these moz.build files are checked in to the tree the project can be built as any other directory: when using a general purpose build backend such as RecursiveMake or FasterMake to build, entries in `GN_DIRS` will be treated as a normal entries in `DIRS`. MozReview-Commit-ID: KlHuP4DY2R4
python/mozbuild/mozbuild/backend/__init__.py
python/mozbuild/mozbuild/backend/common.py
python/mozbuild/mozbuild/frontend/context.py
python/mozbuild/mozbuild/frontend/data.py
python/mozbuild/mozbuild/frontend/emitter.py
python/mozbuild/mozbuild/gn_processor.py
python/mozbuild/mozbuild/test/backend/common.py
python/mozbuild/mozbuild/test/backend/data/gn-processor/gn-configs/x64_False_x64_linux.json
python/mozbuild/mozbuild/test/backend/data/gn-processor/gn-configs/x64_False_x64_mac.json
python/mozbuild/mozbuild/test/backend/data/gn-processor/gn-configs/x64_True_x64_linux.json
python/mozbuild/mozbuild/test/backend/data/gn-processor/ipc/chromium/src/header.h
python/mozbuild/mozbuild/test/backend/data/gn-processor/ipc/glue/header.h
python/mozbuild/mozbuild/test/backend/data/gn-processor/moz.build
python/mozbuild/mozbuild/test/backend/data/gn-processor/trunk/moz.build
python/mozbuild/mozbuild/test/backend/data/gn-processor/trunk/webrtc/build/function.cc
python/mozbuild/mozbuild/test/backend/data/gn-processor/trunk/webrtc/build/no_op_function.cc
python/mozbuild/mozbuild/test/backend/data/gn-processor/trunk/webrtc/modules/include/header.h
python/mozbuild/mozbuild/test/backend/test_gn_processor.py
python/mozbuild/mozbuild/test/python.ini
toolkit/moz.configure
--- a/python/mozbuild/mozbuild/backend/__init__.py
+++ b/python/mozbuild/mozbuild/backend/__init__.py
@@ -3,16 +3,18 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 backends = {
     'ChromeMap': 'mozbuild.codecoverage.chrome_map',
     'CompileDB': 'mozbuild.compilation.database',
     'CppEclipse': 'mozbuild.backend.cpp_eclipse',
     'FasterMake': 'mozbuild.backend.fastermake',
     'FasterMake+RecursiveMake': None,
+    'GnConfigGen': 'mozbuild.gn_processor',
+    'GnMozbuildWriter': 'mozbuild.gn_processor',
     'RecursiveMake': 'mozbuild.backend.recursivemake',
     'TestManifest': 'mozbuild.backend.test_manifest',
     'Tup': 'mozbuild.backend.tup',
     'VisualStudio': 'mozbuild.backend.visualstudio',
 }
 
 
 def get_backend_class(name):
--- a/python/mozbuild/mozbuild/backend/common.py
+++ b/python/mozbuild/mozbuild/backend/common.py
@@ -26,16 +26,17 @@ from mozbuild.frontend.data import (
     ExampleWebIDLInterface,
     Exports,
     IPDLFile,
     FinalTargetPreprocessedFiles,
     FinalTargetFiles,
     GeneratedEventWebIDLFile,
     GeneratedSources,
     GeneratedWebIDLFile,
+    GnProjectData,
     PreprocessedIPDLFile,
     PreprocessedTestWebIDLFile,
     PreprocessedWebIDLFile,
     SharedLibrary,
     TestWebIDLFile,
     UnifiedSources,
     XPIDLFile,
     WebIDLFile,
@@ -309,16 +310,21 @@ class CommonBackend(BuildBackend):
             return False
 
         elif isinstance(obj, Exports):
             objdir_files = [f.full_path for path, files in obj.files.walk() for f in files if isinstance(f, ObjDirPath)]
             if objdir_files:
                 self._handle_generated_sources(objdir_files)
             return False
 
+        elif isinstance(obj, GnProjectData):
+            # These are only handled by special purpose build backends,
+            # ignore them here.
+            return True
+
         else:
             return False
 
         return True
 
     def consume_finished(self):
         if len(self._idl_manager.idls):
             self._handle_idl_manager(self._idl_manager)
--- a/python/mozbuild/mozbuild/frontend/context.py
+++ b/python/mozbuild/mozbuild/frontend/context.py
@@ -2037,16 +2037,34 @@ VARIABLES = {
             GYP_DIRS['foo'].input = 'foo/foo.gyp'
             GYP_DIRS['foo'].variables = {
                 'foo': 'bar',
                 (...)
             }
             (...)
         """),
 
+    'GN_DIRS': (StrictOrderingOnAppendListWithFlagsFactory({
+            'variables': dict,
+            'sandbox_vars': dict,
+            'non_unified_sources': StrictOrderingOnAppendList,
+            'mozilla_flags': list,
+        }), list,
+        """List of dirs containing gn files describing targets to build. Attributes:
+            - variables, a dictionary containing variables and values to pass
+              to `gn gen`.
+            - sandbox_vars, a dictionary containing variables and values to
+              pass to the mozbuild processor on top of those derived from gn.
+            - non_unified_sources, a list containing sources files, relative to
+              the current moz.build, that should be excluded from source file
+              unification.
+            - mozilla_flags, a set of flags that if present in the gn config
+              will be mirrored to the resulting mozbuild configuration.
+        """),
+
     'SPHINX_TREES': (dict, dict,
         """Describes what the Sphinx documentation tree will look like.
 
         Keys are relative directories inside the final Sphinx documentation
         tree to install files into. Values are directories (relative to this
         file) whose content to copy into the Sphinx documentation tree.
         """),
 
--- a/python/mozbuild/mozbuild/frontend/data.py
+++ b/python/mozbuild/mozbuild/frontend/data.py
@@ -1158,8 +1158,18 @@ class ChromeManifestEntry(ContextDerived
         ContextDerived.__init__(self, context)
         assert isinstance(entry, ManifestEntry)
         self.path = mozpath.join(self.install_target, manifest_path)
         # Ensure the entry is relative to the directory containing the
         # manifest path.
         entry = entry.rebase(mozpath.dirname(manifest_path))
         # Then add the install_target to the entry base directory.
         self.entry = entry.move(mozpath.dirname(self.path))
+
+
+class GnProjectData(ContextDerived):
+    def __init__(self, context, target_dir, gn_dir_attrs, non_unified_sources):
+        ContextDerived.__init__(self, context)
+        self.target_dir = target_dir
+        self.non_unified_sources = non_unified_sources
+        self.gn_input_variables = gn_dir_attrs.variables
+        self.gn_sandbox_variables = gn_dir_attrs.sandbox_vars
+        self.mozilla_flags = gn_dir_attrs.mozilla_flags
--- a/python/mozbuild/mozbuild/frontend/emitter.py
+++ b/python/mozbuild/mozbuild/frontend/emitter.py
@@ -38,16 +38,17 @@ from .data import (
     DirectoryTraversal,
     Exports,
     FinalTargetFiles,
     FinalTargetPreprocessedFiles,
     GeneratedEventWebIDLFile,
     GeneratedFile,
     GeneratedSources,
     GeneratedWebIDLFile,
+    GnProjectData,
     ExampleWebIDLInterface,
     ExternalStaticLibrary,
     ExternalSharedLibrary,
     HostDefines,
     HostLibrary,
     HostProgram,
     HostRustProgram,
     HostSimpleProgram,
@@ -552,16 +553,36 @@ class TreeMetadataEmitter(LoggingMixin):
             raise SandboxValidationError(
                 'features for %s should not contain duplicates: %s' % (libname, features),
                 context)
 
         return cls(context, libname, cargo_file, crate_type, dependencies,
                    features, cargo_target_dir, **static_args)
 
 
+    def _handle_gn_dirs(self, context):
+        for target_dir in context.get('GN_DIRS', []):
+            context['DIRS'] += [target_dir]
+            gn_dir = context['GN_DIRS'][target_dir]
+            for v in ('variables',):
+                if not getattr(gn_dir, 'variables'):
+                    raise SandboxValidationError('Missing value for '
+                                                 'GN_DIRS["%s"].%s' % (target_dir, v), context)
+
+            non_unified_sources = set()
+            for s in gn_dir.non_unified_sources:
+                source = SourcePath(context, s)
+                if not os.path.exists(source.full_path):
+                    raise SandboxValidationError('Cannot find %s.' % source,
+                                                 context)
+                non_unified_sources.add(mozpath.join(context.relsrcdir, s))
+
+            yield GnProjectData(context, target_dir, gn_dir, non_unified_sources)
+
+
     def _handle_linkables(self, context, passthru, generated_files):
         linkables = []
         host_linkables = []
         def add_program(prog, var):
             if var.startswith('HOST_'):
                 host_linkables.append(prog)
             else:
                 linkables.append(prog)
@@ -957,16 +978,19 @@ class TreeMetadataEmitter(LoggingMixin):
 
         # We only want to emit an InstallationTarget if one of the consulted
         # variables is defined. Later on, we look up FINAL_TARGET, which has
         # the side-effect of populating it. So, we need to do this lookup
         # early.
         if any(k in context for k in ('FINAL_TARGET', 'XPI_NAME', 'DIST_SUBDIR')):
             yield InstallationTarget(context)
 
+        for obj in self._handle_gn_dirs(context):
+            yield obj
+
         # We always emit a directory traversal descriptor. This is needed by
         # the recursive make backend.
         for o in self._emit_directory_traversal_from_context(context): yield o
 
         for obj in self._process_xpidl(context):
             yield obj
 
         computed_flags = ComputedFlags(context, context['COMPILE_FLAGS'])
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/gn_processor.py
@@ -0,0 +1,576 @@
+# 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 print_function
+
+from collections import defaultdict
+from copy import deepcopy
+import glob
+import json
+import os
+import subprocess
+import sys
+import types
+
+from mozbuild.backend.base import BuildBackend
+import mozpack.path as mozpath
+from mozbuild.frontend.sandbox import alphabetical_sorted
+from mozbuild.frontend.data import GnProjectData
+from mozbuild.util import expand_variables, mkdir
+
+
+license_header = """# 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/.
+"""
+
+generated_header = """
+  ### This moz.build was AUTOMATICALLY GENERATED from a GN config,  ###
+  ### DO NOT edit it by hand.                                       ###
+"""
+
+
+class MozbuildWriter(object):
+    def __init__(self, fh):
+        self._fh = fh
+        self.indent = ''
+        self._indent_increment = 4
+
+        # We need to correlate a small amount of state here to figure out
+        # which library template to use ("Library()" or "SharedLibrary()")
+        self._library_name = None
+        self._shared_library = None
+
+    def mb_serialize(self, v):
+        if isinstance(v, (bool, list)):
+            return repr(v)
+        return '"%s"' % v
+
+    def finalize(self):
+        if self._library_name:
+            self.write('\n')
+            if self._shared_library:
+                self.write_ln("SharedLibrary(%s)" % self.mb_serialize(self._library_name))
+            else:
+                self.write_ln("Library(%s)" % self.mb_serialize(self._library_name))
+
+    def write(self, content):
+        self._fh.write(content)
+
+    def write_ln(self, line):
+        self.write(self.indent)
+        self.write(line)
+        self.write('\n')
+
+    def write_attrs(self, context_attrs):
+        for k, v in context_attrs.iteritems():
+            if isinstance(v, (list, set)):
+                self.write_mozbuild_list(k, alphabetical_sorted(v))
+            elif isinstance(v, dict):
+                self.write_mozbuild_dict(k, v)
+            else:
+                self.write_mozbuild_value(k, v)
+
+    def write_mozbuild_list(self, key, value):
+        if value:
+            self.write('\n')
+            self.write(self.indent + key)
+            self.write(' += [\n    ' + self.indent)
+            self.write((',\n    ' + self.indent).join(self.mb_serialize(v) for v in value))
+            self.write('\n')
+            self.write_ln(']')
+
+    def write_mozbuild_value(self, key, value):
+        if value:
+            if key == 'LIBRARY_NAME':
+                self._library_name = value
+            elif key == 'FORCE_SHARED_LIB':
+                self._shared_library = True
+            else:
+                self.write('\n')
+                self.write_ln('%s = %s' % (key, self.mb_serialize(value)))
+                self.write('\n')
+
+    def write_mozbuild_dict(self, key, value):
+        # Templates we need to use instead of certain values.
+        replacements = (
+            (('COMPILE_FLAGS', '"WARNINGS_AS_ERRORS"', '[]'), 'AllowCompilerWarnings()'),
+        )
+        if value:
+            self.write('\n')
+            for k, v in value.iteritems():
+                subst_vals = key, self.mb_serialize(k), self.mb_serialize(v)
+                wrote_ln = False
+                for flags, tmpl in replacements:
+                    if subst_vals == flags:
+                        self.write_ln(tmpl)
+                        wrote_ln = True
+
+                if not wrote_ln:
+                    self.write_ln("%s[%s] = %s" % subst_vals)
+
+
+    def write_condition(self, values):
+        def mk_condition(k, v):
+            if not v:
+                return 'not CONFIG["%s"]' % k
+            return 'CONFIG["%s"] == %s' % (k, self.mb_serialize(v))
+
+        self.write('\n')
+        self.write('if ')
+        self.write(' and '.join(mk_condition(k, v) for k, v in values.items()))
+        self.write(':\n')
+        self.indent += ' ' * self._indent_increment
+
+    def terminate_condition(self):
+        assert len(self.indent) >= self._indent_increment
+        self.indent = self.indent[self._indent_increment:]
+
+
+def find_deps(all_targets, target):
+    all_deps = set([target])
+    for dep in all_targets[target]['deps']:
+        if dep not in all_deps:
+            all_deps |= find_deps(all_targets, dep)
+    return all_deps
+
+
+def filter_gn_config(gn_result, config, sandbox_vars, input_vars):
+    # Translates the raw output of gn into just what we'll need to generate a
+    # mozbuild configuration.
+    gn_out = {
+        'targets': {},
+        'sandbox_vars': sandbox_vars,
+        'gn_gen_args': input_vars,
+    }
+
+    gn_mozbuild_vars = (
+        'MOZ_DEBUG',
+        'OS_TARGET',
+        'HOST_CPU_ARCH',
+        'CPU_ARCH',
+    )
+
+    mozbuild_args = {k: config.substs.get(k) for k in gn_mozbuild_vars}
+    gn_out['mozbuild_args'] = mozbuild_args
+    all_deps = find_deps(gn_result['targets'], "//:default")
+
+    for target_fullname in all_deps:
+        raw_spec = gn_result['targets'][target_fullname]
+
+        # TODO: 'executable' will need to be handled here at some point as well.
+        if raw_spec['type'] not in ('static_library', 'shared_library',
+                                    'source_set'):
+            continue
+
+        spec = {}
+        for spec_attr in ('type', 'sources', 'defines', 'include_dirs',
+                          'cflags', 'deps', 'libs'):
+            spec[spec_attr] = raw_spec.get(spec_attr, [])
+            gn_out['targets'][target_fullname] = spec
+
+    return gn_out
+
+
+def process_gn_config(gn_config, srcdir, config, output, non_unified_sources,
+                      sandbox_vars, mozilla_flags):
+    # Translates a json gn config into attributes that can be used to write out
+    # moz.build files for this configuration.
+
+    # Much of this code is based on similar functionality in `gyp_reader.py`.
+
+    mozbuild_attrs = {'mozbuild_args': gn_config.get('mozbuild_args', None),
+                      'dirs': {}}
+
+    targets = gn_config["targets"]
+
+    project_relsrcdir = mozpath.relpath(srcdir, config.topsrcdir)
+
+    def target_info(fullname):
+        path, name = target_fullname.split(':')
+        # Stripping '//' gives us a path relative to the project root,
+        # adding a suffix avoids name collisions with libraries already
+        # in the tree (like "webrtc").
+        return path.lstrip('//'), name + '_gn'
+
+    # Process all targets from the given gn project and its dependencies.
+    for target_fullname, spec in targets.iteritems():
+
+        target_path, target_name = target_info(target_fullname)
+        context_attrs = {}
+
+        # Remove leading 'lib' from the target_name if any, and use as
+        # library name.
+        name = target_name
+        if spec['type'] in ('static_library', 'shared_library', 'source_set'):
+            if name.startswith('lib'):
+                name = name[3:]
+            context_attrs['LIBRARY_NAME'] = name.decode('utf-8')
+        else:
+            raise Exception('The following GN target type is not currently '
+                            'consumed by moz.build: "%s". It may need to be '
+                            'added, or you may need to re-run the '
+                            '`GnConfigGen` step.' % spec['type'])
+
+        if spec['type'] == 'shared_library':
+            context_attrs['FORCE_SHARED_LIB'] = True
+
+        sources = []
+        unified_sources = []
+        extensions = set()
+        use_defines_in_asflags = False
+
+        for f in spec.get('sources', []):
+            f = f.lstrip("//")
+            ext = mozpath.splitext(f)[-1]
+            extensions.add(ext)
+            src = '%s/%s' % (project_relsrcdir, f)
+            if ext == '.h':
+                continue
+            elif ext == '.def':
+                context_attrs['SYMBOLS_FILE'] = src
+            elif ext != '.S' and src not in non_unified_sources:
+                unified_sources.append('/%s' % src)
+            else:
+                sources.append('/%s' % src)
+            # The Mozilla build system doesn't use DEFINES for building
+            # ASFILES.
+            if ext == '.s':
+                use_defines_in_asflags = True
+
+        context_attrs['SOURCES'] = sources
+        context_attrs['UNIFIED_SOURCES'] = unified_sources
+
+        context_attrs['DEFINES'] = {}
+        for define in spec.get('defines', []):
+            if '=' in define:
+                name, value = define.split('=', 1)
+                context_attrs['DEFINES'][name] = value
+            else:
+                context_attrs['DEFINES'][define] = True
+
+        context_attrs['LOCAL_INCLUDES'] = []
+        for include in spec.get('include_dirs', []):
+            # GN will have resolved all these paths relative to the root of
+            # the project indicated by "//".
+            if include.startswith('//'):
+                include = include[2:]
+            # moz.build expects all LOCAL_INCLUDES to exist, so ensure they do.
+            if include.startswith('/'):
+                resolved = mozpath.abspath(mozpath.join(config.topsrcdir, include[1:]))
+            else:
+                resolved = mozpath.abspath(mozpath.join(srcdir, include))
+            if not os.path.exists(resolved):
+                # GN files may refer to include dirs that are outside of the
+                # tree or we simply didn't vendor. Print a warning in this case.
+                if not resolved.endswith('gn-output/gen'):
+                    print("Included path: '%s' does not exist, dropping include from GN "
+                          "configuration." % resolved, file=sys.stderr)
+                continue
+            if not include.startswith('/'):
+                include = '/%s/%s' % (project_relsrcdir, include)
+            context_attrs['LOCAL_INCLUDES'] += [include]
+
+        context_attrs['ASFLAGS'] = spec.get('asflags_mozilla', [])
+        if use_defines_in_asflags and defines:
+            context_attrs['ASFLAGS'] += ['-D' + d for d in defines]
+        flags = [f for f in spec.get('cflags', []) if f in mozilla_flags]
+        if flags:
+            suffix_map = {
+                '.c': 'CFLAGS',
+                '.cpp': 'CXXFLAGS',
+                '.cc': 'CXXFLAGS',
+                '.m': 'CMFLAGS',
+                '.mm': 'CMMFLAGS',
+            }
+            variables = (suffix_map[e] for e in extensions if e in suffix_map)
+            for var in variables:
+                for f in flags:
+                    # We may be getting make variable references out of the
+                    # gn data, and we don't want those in emitted data, so
+                    # substitute them with their actual value.
+                    f = expand_variables(f, config.substs).split()
+                    if not f:
+                        continue
+                    # the result may be a string or a list.
+                    if isinstance(f, types.StringTypes):
+                        context_attrs.setdefault(var, []).append(f)
+                    else:
+                        context_attrs.setdefault(var, []).extend(f)
+
+        context_attrs['OS_LIBS'] = []
+        for lib in spec.get('libs', []):
+            lib_name = os.path.splitext(lib)[0]
+            if lib.endswith('.framework'):
+                context_attrs['OS_LIBS'] += ['-framework ' + lib_name]
+            else:
+                context_attrs['OS_LIBS'] += [lib_name]
+
+        # Add some features to all contexts. Put here in case LOCAL_INCLUDES
+        # order matters.
+        context_attrs['LOCAL_INCLUDES'] += [
+            '!/ipc/ipdl/_ipdlheaders',
+            '/ipc/chromium/src',
+            '/ipc/glue',
+        ]
+        # These get set via VC project file settings for normal GYP builds.
+        # TODO: Determine if these defines are needed for GN builds.
+        if gn_config['mozbuild_args']['OS_TARGET'] == 'WINNT':
+            context_attrs['DEFINES']['UNICODE'] = True
+            context_attrs['DEFINES']['_UNICODE'] = True
+
+        context_attrs['COMPILE_FLAGS'] = {
+            'STL': [],
+            'OS_INCLUDES': [],
+        }
+
+        for key, value in sandbox_vars.items():
+            if context_attrs.get(key) and isinstance(context_attrs[key], list):
+                # If we have a key from sandbox_vars that's also been
+                # populated here we use the value from sandbox_vars as our
+                # basis rather than overriding outright.
+                context_attrs[key] = value + context_attrs[key]
+            elif context_attrs.get(key) and isinstance(context_attrs[key], dict):
+                context_attrs[key].update(value)
+            else:
+                context_attrs[key] = value
+
+        target_relsrcdir = mozpath.join(project_relsrcdir, target_path, target_name)
+        mozbuild_attrs['dirs'][target_relsrcdir] = context_attrs
+
+    return mozbuild_attrs
+
+
+def find_common_attrs(config_attributes):
+    # Returns the intersection of the given configs and prunes the inputs
+    # to no longer contain these common attributes.
+
+    common_attrs = deepcopy(config_attributes[0])
+
+    def make_intersection(reference, input_attrs):
+        # Modifies `reference` so that after calling this function it only
+        # contains parts it had in common with in `input_attrs`.
+
+        for k, input_value in input_attrs.items():
+            # Anything in `input_attrs` must match what's already in
+            # `reference`.
+            common_value = reference.get(k)
+            if common_value:
+                if isinstance(input_value, list):
+                    input_value = set(input_value)
+                    reference[k] = [i for i in common_value if i in input_value]
+                elif isinstance(input_value, dict):
+                    reference[k] = {key: value for key, value in common_value.items()
+                                    if key in input_value and value == input_value[key]}
+                elif input_value != common_value:
+                    del reference[k]
+            elif k in reference:
+                del reference[k]
+
+        # Additionally, any keys in `reference` that aren't in `input_attrs`
+        # must be deleted.
+        for k in set(reference.keys()) - set(input_attrs.keys()):
+            del reference[k]
+
+    def make_difference(reference, input_attrs):
+        # Modifies `input_attrs` so that after calling this function it contains
+        # no parts it has in common with in `reference`.
+        for k, input_value in input_attrs.items():
+            common_value = reference.get(k)
+            if common_value:
+                if isinstance(input_value, list):
+                    common_value = set(common_value)
+                    input_attrs[k] = [i for i in input_value if i not in common_value]
+                elif isinstance(input_value, dict):
+                    input_attrs[k] = {key: value for key, value in input_value.items()
+                                      if key not in common_value}
+                else:
+                    del input_attrs[k]
+
+    for config_attr_set in config_attributes[1:]:
+        make_intersection(common_attrs, config_attr_set)
+
+    for config_attr_set in config_attributes:
+        make_difference(common_attrs, config_attr_set)
+
+    return common_attrs
+
+
+def write_mozbuild(config, srcdir, output, non_unified_sources, gn_config_files,
+                   mozilla_flags):
+
+    all_mozbuild_results = []
+
+    for path in gn_config_files:
+        with open(path, 'r') as fh:
+            gn_config = json.load(fh)
+            mozbuild_attrs = process_gn_config(gn_config, srcdir, config,
+                                               output, non_unified_sources,
+                                               gn_config['sandbox_vars'],
+                                               mozilla_flags)
+            all_mozbuild_results.append(mozbuild_attrs)
+
+    # Translate {config -> {dirs -> build info}} into
+    #           {dirs -> [(config, build_info)]}
+    configs_by_dir = defaultdict(list)
+    for config_attrs in all_mozbuild_results:
+        mozbuild_args = config_attrs['mozbuild_args']
+        dirs = config_attrs['dirs']
+        for d, build_data in dirs.items():
+            configs_by_dir[d].append((mozbuild_args, build_data))
+
+    for relsrcdir, configs in configs_by_dir.items():
+        target_srcdir = mozpath.join(config.topsrcdir, relsrcdir)
+        mkdir(target_srcdir)
+
+        target_mozbuild = mozpath.join(target_srcdir, 'moz.build')
+        with open(target_mozbuild, 'w') as fh:
+            mb = MozbuildWriter(fh)
+            mb.write(license_header)
+            mb.write('\n')
+            mb.write(generated_header)
+
+            all_attr_sets = [attrs for _, attrs in configs]
+            all_args = [args for args, _ in configs]
+
+            # Start with attributes that will be a part of the mozconfig
+            # for every configuration, then factor by other potentially useful
+            # combinations.
+            for attrs in ((),
+                          ('MOZ_DEBUG',), ('OS_TARGET',), ('MOZ_DEBUG', 'OS_TARGET',),
+                          ('MOZ_DEBUG', 'OS_TARGET', 'CPU_ARCH', 'HOST_CPU_ARCH')):
+                conditions = set()
+                for args in all_args:
+                    cond = tuple(((k, args.get(k)) for k in attrs))
+                    conditions.add(cond)
+                for cond in conditions:
+                    common_attrs = find_common_attrs([attrs for args, attrs in configs if
+                                                      all(args.get(k) == v for k, v in cond)])
+                    if any(common_attrs.values()):
+                        if cond:
+                            mb.write_condition(dict(cond))
+                        mb.write_attrs(common_attrs)
+                        if cond:
+                            mb.terminate_condition()
+
+            mb.finalize()
+
+    dirs_mozbuild = mozpath.join(srcdir, 'moz.build')
+    with open(dirs_mozbuild, 'w') as fh:
+        mb = MozbuildWriter(fh)
+        mb.write(license_header)
+        mb.write('\n')
+        mb.write(generated_header)
+
+        # Not every srcdir is present for every config, which needs to be
+        # reflected in the generated root moz.build.
+        dirs_by_config = {tuple(v['mozbuild_args'].items()): set(v['dirs'].keys())
+                          for v in all_mozbuild_results}
+
+        for attrs in ((), ('OS_TARGET',), ('OS_TARGET', 'CPU_ARCH')):
+
+            conditions = set()
+            for args in dirs_by_config.keys():
+                cond = tuple(((k, dict(args).get(k)) for k in attrs))
+                conditions.add(cond)
+
+            for cond in conditions:
+                common_dirs = None
+                for args, dir_set in dirs_by_config.items():
+                    if all(dict(args).get(k) == v for k, v in cond):
+                        if common_dirs is None:
+                            common_dirs = deepcopy(dir_set)
+                        else:
+                            common_dirs &= dir_set
+
+                for args, dir_set in dirs_by_config.items():
+                    if all(dict(args).get(k) == v for k, v in cond):
+                        dir_set -= common_dirs
+
+                if common_dirs:
+                    if cond:
+                        mb.write_condition(dict(cond))
+                    mb.write_mozbuild_list('DIRS',
+                                           ['/%s' % d for d in common_dirs])
+                    if cond:
+                        mb.terminate_condition()
+
+
+def generate_gn_config(config, srcdir, output, non_unified_sources, gn_binary,
+                       input_variables, sandbox_variables):
+
+    def str_for_arg(v):
+        if v in (True, False):
+            return str(v).lower()
+        return '"%s"' % v
+
+    gn_args = '--args=%s' % ' '.join(['%s=%s' % (k, str_for_arg(v)) for k, v
+                                      in input_variables.iteritems()])
+    gn_arg_string = '_'.join([str(input_variables[k]) for k in sorted(input_variables.keys())])
+    out_dir = mozpath.join(config.topobjdir, 'gn-output')
+    gen_args = [
+        config.substs['GN'], 'gen', out_dir, gn_args, '--ide=json',
+    ]
+    print("Running \"%s\"" % ' '.join(gen_args), file=sys.stderr)
+    subprocess.check_call(gen_args, cwd=srcdir, stderr=subprocess.STDOUT)
+
+
+    gn_config_file = mozpath.join(out_dir, 'project.json')
+
+    with open(gn_config_file, 'r') as fh:
+        gn_out = json.load(fh)
+        gn_out = filter_gn_config(gn_out, config, sandbox_variables,
+                                  input_variables)
+
+    os.remove(gn_config_file)
+
+    gn_out_file = mozpath.join(out_dir, gn_arg_string + '.json')
+    with open(gn_out_file, 'w') as fh:
+        json.dump(gn_out, fh, indent=4, sort_keys=True, separators=(',', ': '))
+    print("Wrote gn config to %s" % gn_out_file)
+
+
+class GnConfigGenBackend(BuildBackend):
+
+    def consume_object(self, obj):
+        if isinstance(obj, GnProjectData):
+            gn_binary = obj.config.substs.get('GN')
+            if not gn_binary:
+                raise Exception("The GN program must be present to generate GN configs.")
+
+            generate_gn_config(obj.config, mozpath.join(obj.srcdir, obj.target_dir),
+                               mozpath.join(obj.objdir, obj.target_dir),
+                               obj.non_unified_sources, gn_binary,
+                               obj.gn_input_variables, obj.gn_sandbox_variables)
+        return True
+
+    def consume_finished(self):
+        pass
+
+
+class GnMozbuildWriterBackend(BuildBackend):
+
+    def consume_object(self, obj):
+        if isinstance(obj, GnProjectData):
+            gn_config_files = glob.glob(mozpath.join(obj.srcdir, 'gn-configs', '*.json'))
+            if not gn_config_files:
+                # Check the objdir for a gn-config in to aide debugging in cases
+                # someone is running both steps on the same machine and want to
+                # sanity check moz.build generation for a particular config.
+                gn_config_files = glob.glob(mozpath.join(obj.topobjdir,
+                                                         'gn-output', '*.json'))
+            if gn_config_files:
+                print("Writing moz.build files based on the following gn configs: %s" %
+                      gn_config_files)
+                write_mozbuild(obj.config, mozpath.join(obj.srcdir, obj.target_dir),
+                               mozpath.join(obj.objdir, obj.target_dir),
+                               obj.non_unified_sources, gn_config_files,
+                               obj.mozilla_flags)
+            else:
+                print("Ignoring gn project '%s', no config files found in '%s'" %
+                      (obj.srcdir, mozpath.join(obj.srcdir, 'gn-configs')))
+        return True
+
+    def consume_finished(self):
+        pass
--- a/python/mozbuild/mozbuild/test/backend/common.py
+++ b/python/mozbuild/mozbuild/test/backend/common.py
@@ -164,17 +164,35 @@ CONFIGS = defaultdict(lambda: {
     'prog-lib-c-only': {
         'defines': {},
         'non_global_defines': [],
         'substs': {
             'COMPILE_ENVIRONMENT': '1',
             'LIB_SUFFIX': '.a',
             'BIN_SUFFIX': '',
         },
-    }
+    },
+    'gn-processor': {
+        'defines': {},
+        'non_global_defines': [],
+        'substs': {
+            'BUILD_BACKENDS': [
+                'GnMozbuildWriter',
+                'RecursiveMake',
+            ],
+            'COMPILE_ENVIRONMENT': '1',
+            'STL_FLAGS': [],
+            'RUST_TARGET': 'x86_64-unknown-linux-gnu',
+            'LIB_PREFIX': 'lib',
+            'RUST_LIB_PREFIX': 'lib',
+            'LIB_SUFFIX': 'a',
+            'RUST_LIB_SUFFIX': 'a',
+            'OS_TARGET': 'Darwin',
+        },
+    },
 })
 
 
 class BackendTester(unittest.TestCase):
     def setUp(self):
         self._old_env = dict(os.environ)
         os.environ.pop('MOZ_OBJDIR', None)
 
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/gn-processor/gn-configs/x64_False_x64_linux.json
@@ -0,0 +1,47 @@
+{
+  "mozbuild_args": {
+    "HOST_CPU_ARCH": "x86_64", 
+    "OS_TARGET": "Linux", 
+    "CPU_ARCH": "x86_64",
+    "MOZ_DEBUG": false
+  }, 
+  "targets": {
+    "//webrtc:webrtc": {
+      "deps": [
+        "//webrtc/base:base"
+      ],
+      "sources": [
+        "//webrtc/build/no_op_function.cc", 
+        "//webrtc/call.h", 
+        "//webrtc/config.h"
+      ], 
+      "cflags": [
+        "-fno-strict-aliasing", 
+        "-fstack-protector"
+      ],
+      "defines": [
+        "USE_NSS_CERTS=1", 
+        "USE_X11=1", 
+        "CHROMIUM_BUILD",
+        "NDEBUG", 
+        "NVALGRIND",
+        "LINUX_NDEBUG"
+      ],
+      "type": "static_library", 
+      "include_dirs": [
+        "//webrtc/modules/include/"
+      ]
+    }
+  },
+  "sandbox_vars": {
+    "FINAL_LIBRARY": "webrtc", 
+    "COMPILE_FLAGS": {
+      "WARNINGS_AS_ERRORS": []
+    }
+  }, 
+  "gn_gen_args": {
+    "host_cpu": "x64", 
+    "target_os": "linux", 
+    "target_cpu": "x64"
+  }
+}
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/gn-processor/gn-configs/x64_False_x64_mac.json
@@ -0,0 +1,59 @@
+{
+  "mozbuild_args": {
+    "HOST_CPU_ARCH": "x86_64", 
+    "CPU_ARCH": "x86_64", 
+    "OS_TARGET": "Darwin",
+    "MOZ_DEBUG": false
+  }, 
+  "targets": {
+    "//webrtc:webrtc": {
+      "deps": [
+        "//webrtc/base:base",
+        "//webrtc/base:mac_base"
+      ],
+      "sources": [
+        "//webrtc/build/no_op_function.cc",
+        "//webrtc/call.h", 
+        "//webrtc/config.h"
+      ], 
+      "cflags": [
+        "-fno-strict-aliasing", 
+        "-fstack-protector"
+      ],
+      "defines": [
+        "USE_NSS_CERTS=1", 
+        "USE_X11=1", 
+        "CHROMIUM_BUILD",
+        "NVALGRIND",
+        "WEBRTC_MAC",
+        "NDEBUG"
+      ],
+      "type": "static_library", 
+      "include_dirs": [
+        "//webrtc/modules/include/"
+      ]
+    },
+    "//webrtc/base:mac_base": {
+      "sources": [
+        "//webrtc/build/function.cc"
+      ],
+      "cflags": [], 
+      "libs": [], 
+      "defines": [], 
+      "type": "static_library", 
+      "include_dirs": [],
+      "deps": []
+    }
+  },
+  "sandbox_vars": {
+    "FINAL_LIBRARY": "webrtc", 
+    "COMPILE_FLAGS": {
+      "WARNINGS_AS_ERRORS": []
+    }
+  }, 
+  "gn_gen_args": {
+    "host_cpu": "x64", 
+    "target_os": "mac", 
+    "target_cpu": "x64"
+  }
+}
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/gn-processor/gn-configs/x64_True_x64_linux.json
@@ -0,0 +1,46 @@
+{
+  "mozbuild_args": {
+    "HOST_CPU_ARCH": "x86_64", 
+    "OS_TARGET": "Linux", 
+    "CPU_ARCH": "x86_64",
+    "MOZ_DEBUG": "1"
+  }, 
+  "targets": {
+    "//webrtc:webrtc": {
+      "deps": [
+        "//webrtc/base:base"
+      ],
+      "sources": [
+        "//webrtc/build/no_op_function.cc", 
+        "//webrtc/call.h", 
+        "//webrtc/config.h"
+      ], 
+      "cflags": [
+        "-fno-strict-aliasing", 
+        "-fstack-protector"
+      ],
+      "defines": [
+        "USE_NSS_CERTS=1", 
+        "USE_X11=1", 
+        "CHROMIUM_BUILD",
+        "MOZ_LINUX_AND_DEBUG"
+      ],
+      "type": "static_library", 
+      "include_dirs": [
+        "//webrtc/modules/include/"
+      ]
+    }
+  },
+  "sandbox_vars": {
+    "FINAL_LIBRARY": "webrtc", 
+    "COMPILE_FLAGS": {
+      "WARNINGS_AS_ERRORS": []
+    }
+  }, 
+  "gn_gen_args": {
+    "is_debug": true, 
+    "host_cpu": "x64", 
+    "target_os": "linux", 
+    "target_cpu": "x64"
+  }
+}
new file mode 100644
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/gn-processor/moz.build
@@ -0,0 +1,47 @@
+# -*- Mode: python; 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/.
+
+@template
+def SharedLibrary(name):
+    FORCE_SHARED_LIB = True
+    LIBRARY_NAME = name
+
+@template
+def Library(name):
+    LIBRARY_NAME = name
+
+@template
+def AllowCompilerWarnings():
+    COMPILE_FLAGS['WARNINGS_AS_ERRORS'] = []
+
+gn_vars = {}
+if CONFIG['MOZ_DEBUG']:
+   gn_vars['is_debug'] = True
+else:
+   gn_vars['is_debug'] = False
+
+os = CONFIG['OS_TARGET']
+flavors = {
+    'Linux': 'linux',
+    'Darwin': 'mac' if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa' else 'ios',
+}
+gn_vars['target_os'] = flavors.get(os)
+
+arches = {
+    'x86_64': 'x64',
+}
+
+gn_vars['host_cpu'] = arches.get(CONFIG['HOST_CPU_ARCH'], CONFIG['HOST_CPU_ARCH'])
+gn_vars['target_cpu'] = arches.get(CONFIG['CPU_ARCH'], CONFIG['CPU_ARCH'])
+
+
+GN_DIRS += ['trunk']
+
+GN_DIRS['trunk'].variables = gn_vars
+GN_DIRS['trunk'].mozilla_flags = ['-fstack-protector']
+GN_DIRS['trunk'].non_unified_sources += ['trunk/webrtc/build/function.cc']
+
+Library('webrtc')
new file mode 100644
new file mode 100644
new file mode 100644
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/test_gn_processor.py
@@ -0,0 +1,201 @@
+# 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 os
+import shutil
+import tempfile
+import unittest
+
+from copy import deepcopy
+
+from mozunit import main
+
+from mozbuild.test.backend.common import BackendTester
+
+import mozpack.path as mozpath
+
+from mozbuild.gn_processor import (
+    GnMozbuildWriterBackend,
+    find_common_attrs,
+)
+from mozbuild.backend.recursivemake import RecursiveMakeBackend
+
+from mozbuild.frontend.data import (
+    ComputedFlags,
+    StaticLibrary,
+    Sources,
+    UnifiedSources,
+)
+
+class TestGnMozbuildWriter(BackendTester):
+
+    def setUp(self):
+        self._backup_dir = tempfile.mkdtemp(prefix='mozbuild')
+        self._gn_dir = os.path.join(os.path.dirname(__file__),
+                                    'data', 'gn-processor', 'trunk')
+        shutil.copytree(os.path.join(self._gn_dir),
+                        os.path.join(self._backup_dir, 'trunk'))
+        super(TestGnMozbuildWriter, self).setUp()
+
+    def tearDown(self):
+        shutil.rmtree(self._gn_dir)
+        shutil.copytree(os.path.join(self._backup_dir, 'trunk'),
+                        self._gn_dir)
+        shutil.rmtree(self._backup_dir)
+        super(TestGnMozbuildWriter, self).tearDown()
+
+    def test_gn_processor(self):
+        # Generate our moz.build files.
+        env = self._get_environment('gn-processor')
+        self._consume('gn-processor', GnMozbuildWriterBackend, env=env)
+
+        # Read the resulting tree.
+        _, objs = self._emit('gn-processor', env=env)
+
+        # The config for this srcdir sets OS_TARGET = Darwin, for which our gn config
+        # generates two trivial libraries that link in to the main webrtc library,
+        # one source file, one unified source file, and one directory with some
+        # local defines and includes set. The following asserts this is what we see when
+        # we read the generated moz.build files.
+        objs = list(objs)
+        expected_srcdirs = set([
+            mozpath.join(env.topsrcdir, p)
+            for p in ['trunk',
+                      'trunk/webrtc/webrtc_gn',
+                      'trunk/webrtc/base/mac_base_gn']
+        ] + [env.topsrcdir])
+        actual_srcdirs = set([
+            o.srcdir for o in objs
+        ])
+        self.assertEqual(expected_srcdirs, actual_srcdirs)
+
+        libs = {o.lib_name: o for o in objs if isinstance(o, StaticLibrary)}
+        expected_libs = set(['libwebrtc.a', 'libwebrtc_gn.a', 'libmac_base_gn.a'])
+        self.assertEqual(expected_libs, set(libs.keys()))
+        # All libs are linked into 'libwebrtc.a'.
+        self.assertEqual(set(libs['libwebrtc.a'].linked_libraries),
+                         set(libs.values()) - set([libs['libwebrtc.a']]))
+
+        [sources] = [o for o in objs if isinstance(o, Sources)]
+        [unified_sources] = [o for o in objs if isinstance(o, UnifiedSources)]
+        self.assertEqual(sources.files,
+                         [mozpath.join(sources.topsrcdir, 'trunk', 'webrtc',
+                                       'build', 'function.cc')])
+        self.assertEqual(unified_sources.files,
+                         [mozpath.join(unified_sources.topsrcdir, 'trunk', 'webrtc',
+                                       'build', 'no_op_function.cc')])
+
+        [flags_obj] = [o for o in objs if isinstance(o, ComputedFlags)
+                       if 'DEFINES' in o.flags and o.flags['DEFINES']]
+        self.assertEqual(flags_obj.srcdir, mozpath.join(flags_obj.topsrcdir,
+                                                        'trunk', 'webrtc', 'webrtc_gn'))
+        expected_defines = set([
+            '-DUSE_NSS_CERTS=1',
+            '-DCHROMIUM_BUILD',
+            '-DUSE_X11=1',
+            '-DNDEBUG',
+            '-DNVALGRIND',
+            '-DWEBRTC_MAC',
+        ])
+        self.assertEqual(set(flags_obj.flags['DEFINES']), expected_defines)
+
+        expected_includes = set([
+            '-I' + mozpath.join(flags_obj.topobjdir, 'ipc', 'ipdl', '_ipdlheaders'),
+            '-I' + mozpath.join(flags_obj.topsrcdir, 'ipc', 'chromium', 'src'),
+            '-I' + mozpath.join(flags_obj.topsrcdir, 'ipc', 'glue'),
+            '-I' + mozpath.join(flags_obj.topsrcdir, 'trunk', 'webrtc', 'modules', 'include'),
+        ])
+        self.assertEqual(set(flags_obj.flags['LOCAL_INCLUDES']), expected_includes)
+        self.assertEqual(flags_obj.flags['MOZBUILD_CXXFLAGS'], ['-fstack-protector'])
+
+
+class TestGnMozbuildFactoring(unittest.TestCase):
+
+    test_attrs = {
+        'LOCAL_INCLUDES': [
+            '!/ipc/ipdl/_ipdlheaders',
+            '/ipc/chromium/src',
+            '/ipc/glue',
+        ],
+        'DEFINES': {
+            'UNICODE': True,
+            'DEBUG': True,
+        },
+        'LIBRARY_NAME': 'Foo',
+    }
+
+    def test_single_attr_set(self):
+        # If we only have one set of attributes they're all considered common.
+        single_attrs = deepcopy(self.test_attrs)
+        common_attrs = find_common_attrs([single_attrs])
+        self.assertFalse(any(v for v in single_attrs.values()))
+        self.assertEqual(self.test_attrs, common_attrs)
+
+    def test_list_factor(self):
+        list_attrs = deepcopy(self.test_attrs)
+        input_attrs = deepcopy(self.test_attrs)
+        list_attrs['LOCAL_INCLUDES'] += ['/local/include']
+        list_attrs['LOCAL_INCLDUES'] = list(reversed(list_attrs['LOCAL_INCLUDES']))
+        common_attrs = find_common_attrs([input_attrs, list_attrs])
+        self.assertEqual(list_attrs['LOCAL_INCLUDES'], ['/local/include'])
+        self.assertEqual(self.test_attrs, common_attrs)
+        self.assertFalse(any(v for v in input_attrs.values()))
+
+    def test_value_factor(self):
+        input_attrs = deepcopy(self.test_attrs)
+        value_attrs = deepcopy(self.test_attrs)
+        value_attrs['LOCAL_INCLUDES'] = ['/local/include']
+        value_attrs['DEFINES']['DEBUG'] = False
+        value_attrs['DEFINES']['UNICODE'] = False
+        common_attrs = find_common_attrs([value_attrs, input_attrs])
+        self.assertEqual(common_attrs['LIBRARY_NAME'], 'Foo')
+        self.assertNotIn('LIBRARY_NAME', value_attrs)
+        self.assertNotIn('LIBRARY_NAME', input_attrs)
+        del common_attrs['LIBRARY_NAME']
+        self.assertFalse(any(v for v in common_attrs.values()))
+        self.assertEqual(input_attrs['LOCAL_INCLUDES'], self.test_attrs['LOCAL_INCLUDES'])
+        self.assertEqual(input_attrs['DEFINES'], self.test_attrs['DEFINES'])
+        self.assertEqual(value_attrs['LOCAL_INCLUDES'],  ['/local/include'])
+        self.assertEqual(value_attrs['DEFINES'],  {'DEBUG': False, 'UNICODE': False})
+
+    def test_dictionary_factor(self):
+        input_attrs = deepcopy(self.test_attrs)
+        dict_subset_attrs = deepcopy(self.test_attrs)
+        overlapping_dict_attrs = deepcopy(self.test_attrs)
+
+        del dict_subset_attrs['DEFINES']['DEBUG']
+        overlapping_dict_attrs['DEFINES']['MOZ_DEBUG'] = True
+
+        common_attrs = find_common_attrs([input_attrs, dict_subset_attrs, overlapping_dict_attrs])
+        for attr in ('LIBRARY_NAME', 'LOCAL_INCLUDES'):
+            self.assertEqual(common_attrs[attr], self.test_attrs[attr])
+            self.assertFalse(input_attrs.get(attr))
+            self.assertFalse(dict_subset_attrs.get(attr))
+            self.assertFalse(overlapping_dict_attrs.get(attr))
+        self.assertEqual(common_attrs['DEFINES'], {'UNICODE': True})
+        self.assertEqual(dict_subset_attrs['DEFINES'], {})
+        self.assertEqual(overlapping_dict_attrs['DEFINES'], {
+            'DEBUG': True,
+            'MOZ_DEBUG': True,
+        })
+
+    def test_common_attrs(self):
+        input_attrs = deepcopy(self.test_attrs)
+        dict_subset_attrs = deepcopy(self.test_attrs)
+
+        del dict_subset_attrs['DEFINES']
+        common_attrs = find_common_attrs([input_attrs, dict_subset_attrs])
+        self.assertNotIn('DEFINES', common_attrs)
+        expected_common = deepcopy(self.test_attrs)
+        del expected_common['DEFINES']
+        self.assertEqual(expected_common, common_attrs)
+        for attr_set in (input_attrs, dict_subset_attrs):
+            self.assertNotIn('LIBRARY_NAME', attr_set)
+            self.assertEqual(attr_set['LOCAL_INCLUDES'], [])
+
+
+if __name__ == '__main__':
+    main()
--- a/python/mozbuild/mozbuild/test/python.ini
+++ b/python/mozbuild/mozbuild/test/python.ini
@@ -1,16 +1,17 @@
 [action/test_buildlist.py]
 [action/test_generate_browsersearch.py]
 [action/test_langpack_manifest.py]
 [action/test_process_install_manifest.py]
 [action/test_package_fennec_apk.py]
 [backend/test_build.py]
 [backend/test_configenvironment.py]
 [backend/test_fastermake.py]
+[backend/test_gn_processor.py]
 [backend/test_partialconfigenvironment.py]
 [backend/test_recursivemake.py]
 [backend/test_test_manifest.py]
 [backend/test_visualstudio.py]
 [codecoverage/test_lcov_rewrite.py]
 [compilation/test_warnings.py]
 [configure/lint.py]
 [configure/test_checks_configure.py]
--- a/toolkit/moz.configure
+++ b/toolkit/moz.configure
@@ -539,16 +539,17 @@ system_gpsd = pkg_check_modules('MOZ_GPS
 set_config('MOZ_GPSD', depends_if(system_gpsd)(lambda _: True))
 
 # Miscellaneous programs
 # ==============================================================
 
 check_prog('TAR', ('gnutar', 'gtar', 'tar'))
 check_prog('UNZIP', ('unzip',))
 check_prog('ZIP', ('zip',))
+check_prog('GN', ('gn',), allow_missing=True)
 
 # Key files
 # ==============================================================
 include('../build/moz.configure/keyfiles.configure')
 
 simple_keyfile('Mozilla API')
 
 simple_keyfile('Google API')