bug 1160185 - support GENERATED_FILES in EXPORTS. r=glandium
authorTed Mielczarek <ted@mielczarek.org>
Tue, 01 Dec 2015 09:53:16 -0500
changeset 275814 3ca0a1e37264624f4304aec23918fb9e0f4910e2
parent 275813 09d64535bcda005593b0e29fcfe813f07e128b79
child 275815 e648ed99a3a2c93261b8b18647ca445f2e7f869b
push id29768
push usercbook@mozilla.com
push dateMon, 07 Dec 2015 13:16:29 +0000
treeherdermozilla-central@59bc3c7a83de [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersglandium
bugs1160185
milestone45.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 1160185 - support GENERATED_FILES in EXPORTS. r=glandium This change allows specifying objdir-relative paths in EXPORTS to enable exporting entries from GENERATED_FILES. Objdir paths in EXPORTS that are not in GENERATED_FILES will raise an exception. Example: ``` EXPORTS += ['!g.h', 'f.h'] GENERATED_FILES += ['g.h'] ``` Given the implementation, this should also work for FINAL_TARGET_FILES, FINAL_TARGET_PP_FILES, and TESTING_FILES, but those are not well-tested. This patch also renames the install manifest for '_tests' to match the directory name for convenience in some code I refactored.
Makefile.in
accessible/xpcom/Makefile.in
accessible/xpcom/moz.build
python/mozbuild/mozbuild/backend/recursivemake.py
python/mozbuild/mozbuild/frontend/context.py
python/mozbuild/mozbuild/frontend/data.py
python/mozbuild/mozbuild/frontend/emitter.py
python/mozbuild/mozbuild/test/backend/data/exports-generated/dom1.h
python/mozbuild/mozbuild/test/backend/data/exports-generated/foo.h
python/mozbuild/mozbuild/test/backend/data/exports-generated/gfx.h
python/mozbuild/mozbuild/test/backend/data/exports-generated/moz.build
python/mozbuild/mozbuild/test/backend/data/exports-generated/mozilla1.h
python/mozbuild/mozbuild/test/backend/test_recursivemake.py
python/mozbuild/mozbuild/test/frontend/data/exports-generated/foo.h
python/mozbuild/mozbuild/test/frontend/data/exports-generated/moz.build
python/mozbuild/mozbuild/test/frontend/data/exports-generated/mozilla1.h
python/mozbuild/mozbuild/test/frontend/data/exports-missing-generated/foo.h
python/mozbuild/mozbuild/test/frontend/data/exports-missing-generated/moz.build
python/mozbuild/mozbuild/test/frontend/data/exports-missing/foo.h
python/mozbuild/mozbuild/test/frontend/data/exports-missing/moz.build
python/mozbuild/mozbuild/test/frontend/data/exports-missing/mozilla1.h
python/mozbuild/mozbuild/test/frontend/data/exports/bar.h
python/mozbuild/mozbuild/test/frontend/data/exports/baz.h
python/mozbuild/mozbuild/test/frontend/data/exports/dom1.h
python/mozbuild/mozbuild/test/frontend/data/exports/dom2.h
python/mozbuild/mozbuild/test/frontend/data/exports/dom3.h
python/mozbuild/mozbuild/test/frontend/data/exports/foo.h
python/mozbuild/mozbuild/test/frontend/data/exports/gfx.h
python/mozbuild/mozbuild/test/frontend/data/exports/mem.h
python/mozbuild/mozbuild/test/frontend/data/exports/mem2.h
python/mozbuild/mozbuild/test/frontend/data/exports/mozilla1.h
python/mozbuild/mozbuild/test/frontend/data/exports/mozilla2.h
python/mozbuild/mozbuild/test/frontend/data/exports/pprio.h
python/mozbuild/mozbuild/test/frontend/data/exports/pprthred.h
python/mozbuild/mozbuild/test/frontend/data/final-target-pp-files-non-srcdir/moz.build
python/mozbuild/mozbuild/test/frontend/test_emitter.py
--- a/Makefile.in
+++ b/Makefile.in
@@ -131,17 +131,17 @@ endif
 .PHONY: $(addprefix install-,$(install_manifests))
 $(addprefix install-,$(filter dist/%,$(install_manifests))): install-dist/%: $(install_manifest_depends)
 	$(call py_action,process_install_manifest,$(if $(NO_REMOVE),--no-remove )$(DIST)/$* _build_manifests/install/dist_$*)
 
 # Dummy wrapper rule to allow the faster backend to piggy back
 install-dist_%: install-dist/% ;
 
 install-_tests: $(install_manifest_depends)
-	$(call py_action,process_install_manifest,$(if $(NO_REMOVE),--no-remove )_tests _build_manifests/install/tests)
+	$(call py_action,process_install_manifest,$(if $(NO_REMOVE),--no-remove )_tests _build_manifests/install/_tests)
 
 # For compatibility
 .PHONY: install-tests
 install-tests: install-_tests
 
 include $(topsrcdir)/build/moz-automation.mk
 
 # dist and _tests should be purged during cleaning. However, we don't want them
deleted file mode 100644
--- a/accessible/xpcom/Makefile.in
+++ /dev/null
@@ -1,10 +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'd like this to be defined in a future GENERATED_EXPORTS list.
-# Bug 1160185 has a few proposals for this.
-INSTALL_TARGETS += xpcaccevents
-xpcaccevents_FILES := xpcAccEvents.h
-xpcaccevents_DEST = $(DIST)/include
-xpcaccevents_TARGET := export
--- a/accessible/xpcom/moz.build
+++ b/accessible/xpcom/moz.build
@@ -19,16 +19,20 @@ UNIFIED_SOURCES += [
     'xpcAccessibleTextRange.cpp',
     'xpcAccessibleValue.cpp',
 ]
 
 SOURCES += [
     '!xpcAccEvents.cpp',
 ]
 
+EXPORTS += [
+    '!xpcAccEvents.h',
+]
+
 LOCAL_INCLUDES += [
     '/accessible/base',
     '/accessible/generic',
 ]
 
 if CONFIG['MOZ_ENABLE_GTK']:
     LOCAL_INCLUDES += [
         '/accessible/atk',
--- a/python/mozbuild/mozbuild/backend/recursivemake.py
+++ b/python/mozbuild/mozbuild/backend/recursivemake.py
@@ -396,17 +396,17 @@ class RecursiveMakeBackend(CommonBackend
                 'dist_bin',
                 'dist_branding',
                 'dist_idl',
                 'dist_include',
                 'dist_public',
                 'dist_private',
                 'dist_sdk',
                 'dist_xpi-stage',
-                'tests',
+                '_tests',
                 'xpidl',
             ]}
 
         self._traversal = RecursiveMakeTraversal()
         self._compile_graph = defaultdict(set)
 
         self._may_skip = {
             'export': set(),
@@ -504,19 +504,16 @@ class RecursiveMakeBackend(CommonBackend
                         backend_file.write('%s := 1\n' % k)
                 else:
                     backend_file.write('%s := %s\n' % (k, v))
         elif isinstance(obj, HostDefines):
             self._process_defines(obj, backend_file, which='HOST_DEFINES')
         elif isinstance(obj, Defines):
             self._process_defines(obj, backend_file)
 
-        elif isinstance(obj, Exports):
-            self._process_exports(obj, obj.exports, backend_file)
-
         elif isinstance(obj, GeneratedFile):
             dep_file = "%s.pp" % obj.output
             backend_file.write('GENERATED_FILES += %s\n' % obj.output)
             backend_file.write('EXTRA_MDDEPEND_FILES += %s\n' % dep_file)
             if obj.script:
                 backend_file.write("""{output}: {script}{inputs}
 \t$(REPORT_BUILD)
 \t$(call py_action,file_generate,{script} {method} {output} $(MDDEPDIR)/{dep_file}{inputs})
@@ -582,17 +579,17 @@ class RecursiveMakeBackend(CommonBackend
             self._process_static_library(obj, backend_file)
             self._process_linked_libraries(obj, backend_file)
 
         elif isinstance(obj, HostLibrary):
             self._process_host_library(obj, backend_file)
             self._process_linked_libraries(obj, backend_file)
 
         elif isinstance(obj, FinalTargetFiles):
-            self._process_final_target_files(obj, obj.files)
+            self._process_final_target_files(obj, obj.files, backend_file)
 
         elif isinstance(obj, FinalTargetPreprocessedFiles):
             self._process_final_target_pp_files(obj, obj.files, backend_file)
 
         elif isinstance(obj, AndroidResDirs):
             # Order matters.
             for p in obj.paths:
                 backend_file.write('ANDROID_RES_DIRS += %s\n' % p.full_path)
@@ -847,17 +844,17 @@ class RecursiveMakeBackend(CommonBackend
             install_prefix, manifests = t
             manifest_stem = mozpath.join(install_prefix, '%s.ini' % flavor)
             self._write_master_test_manifest(mozpath.join(
                 self.environment.topobjdir, '_tests', manifest_stem),
                 manifests)
 
             # Catch duplicate inserts.
             try:
-                self._install_manifests['tests'].add_optional_exists(manifest_stem)
+                self._install_manifests['_tests'].add_optional_exists(manifest_stem)
             except ValueError:
                 pass
 
         self._write_manifests('install', self._install_manifests)
 
         ensureParentDir(mozpath.join(self.environment.topobjdir, 'dist', 'foo'))
 
     def _pretty_path(self, path, backend_file):
@@ -951,37 +948,25 @@ class RecursiveMakeBackend(CommonBackend
         """Output the DEFINES rules to the given backend file."""
         defines = list(obj.get_defines())
         if defines:
             backend_file.write(which + ' +=')
             for define in defines:
                 backend_file.write(' %s' % define)
             backend_file.write('\n')
 
-    def _process_exports(self, obj, exports, backend_file):
-        # This may not be needed, but is present for backwards compatibility
-        # with the old make rules, just in case.
-        if not obj.dist_install:
-            return
-
-        for source, dest in self._walk_hierarchy(obj, exports):
-            self._install_manifests['dist_include'].add_symlink(source, dest)
-
-            if not os.path.exists(source):
-                raise Exception('File listed in EXPORTS does not exist: %s' % source)
-
     def _process_test_harness_files(self, obj, backend_file):
         for path, files in obj.srcdir_files.iteritems():
             for source in files:
                 dest = '%s/%s' % (path, mozpath.basename(source))
-                self._install_manifests['tests'].add_symlink(source, dest)
+                self._install_manifests['_tests'].add_symlink(source, dest)
 
         for path, patterns in obj.srcdir_pattern_files.iteritems():
             for p in patterns:
-                self._install_manifests['tests'].add_pattern_symlink(p[0], p[1], path)
+                self._install_manifests['_tests'].add_pattern_symlink(p[0], p[1], path)
 
         for path, files in obj.objdir_files.iteritems():
             prefix = 'TEST_HARNESS_%s' % path.replace('/', '_')
             backend_file.write("""
 %(prefix)s_FILES := %(files)s
 %(prefix)s_DEST := %(dest)s
 INSTALL_TARGETS += %(prefix)s
 """ % { 'prefix': prefix,
@@ -1129,32 +1114,32 @@ INSTALL_TARGETS += %(prefix)s
         self.backend_input_files.add(mozpath.join(obj.topsrcdir,
             obj.manifest_relpath))
 
         # Don't allow files to be defined multiple times unless it is allowed.
         # We currently allow duplicates for non-test files or test files if
         # the manifest is listed as a duplicate.
         for source, (dest, is_test) in obj.installs.items():
             try:
-                self._install_manifests['tests'].add_symlink(source, dest)
+                self._install_manifests['_tests'].add_symlink(source, dest)
             except ValueError:
                 if not obj.dupe_manifest and is_test:
                     raise
 
         for base, pattern, dest in obj.pattern_installs:
             try:
-                self._install_manifests['tests'].add_pattern_symlink(base,
+                self._install_manifests['_tests'].add_pattern_symlink(base,
                     pattern, dest)
             except ValueError:
                 if not obj.dupe_manifest:
                     raise
 
         for dest in obj.external_installs:
             try:
-                self._install_manifests['tests'].add_optional_exists(dest)
+                self._install_manifests['_tests'].add_optional_exists(dest)
             except ValueError:
                 if not obj.dupe_manifest:
                     raise
 
         m = self._test_manifests.setdefault(obj.flavor,
             (obj.install_prefix, set()))
         m[1].add(obj.manifest_obj_relpath)
 
@@ -1293,34 +1278,50 @@ INSTALL_TARGETS += %(prefix)s
             if obj.KIND == 'target':
                 backend_file.write_once('OS_LIBS += %s\n' % lib)
             else:
                 backend_file.write_once('HOST_EXTRA_LIBS += %s\n' % lib)
 
         # Process library-based defines
         self._process_defines(obj.defines, backend_file)
 
-    def _process_final_target_files(self, obj, files):
+    def _process_final_target_files(self, obj, files, backend_file):
         target = obj.install_target
-        if target.startswith('dist/bin'):
-            install_manifest = self._install_manifests['dist_bin']
-            reltarget = mozpath.relpath(target, 'dist/bin')
-        elif target.startswith('dist/xpi-stage'):
-            install_manifest = self._install_manifests['dist_xpi-stage']
-            reltarget = mozpath.relpath(target, 'dist/xpi-stage')
-        elif target.startswith('_tests'):
-            install_manifest = self._install_manifests['tests']
-            reltarget = mozpath.relpath(target, '_tests')
+        for path in (
+                'dist/bin',
+                'dist/xpi-stage',
+                '_tests',
+                'dist/include',
+        ):
+            manifest = path.replace('/', '_')
+            if target.startswith(path):
+                install_manifest = self._install_manifests[manifest]
+                reltarget = mozpath.relpath(target, path)
+                break
         else:
             raise Exception("Cannot install to " + target)
 
         for path, files in files.walk():
+            target_var = (mozpath.join(target, path)
+                          if path else target).replace('/', '_')
+            have_objdir_files = False
             for f in files:
-                dest = mozpath.join(reltarget, path, mozpath.basename(f))
-                install_manifest.add_symlink(f.full_path, dest)
+                if not isinstance(f, ObjDirPath):
+                    dest = mozpath.join(reltarget, path, mozpath.basename(f))
+                    install_manifest.add_symlink(f.full_path, dest)
+                else:
+                    backend_file.write('%s_FILES += %s\n' % (
+                        target_var, self._pretty_path(f, backend_file)))
+                    have_objdir_files = True
+            if have_objdir_files:
+                backend_file.write('%s_DEST := $(DEPTH)/%s\n'
+                                   % (target_var,
+                                      mozpath.join(target, path)))
+                backend_file.write('%s_TARGET := export\n' % target_var)
+                backend_file.write('INSTALL_TARGETS += %s\n' % target_var)
 
     def _process_final_target_pp_files(self, obj, files, backend_file):
         # We'd like to install these via manifests as preprocessed files.
         # But they currently depend on non-standard flags being added via
         # some Makefiles, so for now we just pass them through to the
         # underlying Makefile.in.
         for i, (path, files) in enumerate(files.walk()):
             for f in files:
--- a/python/mozbuild/mozbuild/frontend/context.py
+++ b/python/mozbuild/mozbuild/frontend/context.py
@@ -1263,29 +1263,33 @@ VARIABLES = {
     'CONFIGURE_DEFINE_FILES': (ContextDerivedTypedList(SourcePath, StrictOrderingOnAppendList), list,
         """Output files generated from configure/config.status.
 
         This is a substitute for ``AC_CONFIG_HEADER`` in autoconf. This is very
         similar to ``CONFIGURE_SUBST_FILES`` except the generation logic takes
         into account the values of ``AC_DEFINE`` instead of ``AC_SUBST``.
         """, None),
 
-    'EXPORTS': (HierarchicalStringList, list,
+    'EXPORTS': (ContextDerivedTypedHierarchicalStringList(Path), list,
         """List of files to be exported, and in which subdirectories.
 
         ``EXPORTS`` is generally used to list the include files to be exported to
         ``dist/include``, but it can be used for other files as well. This variable
         behaves as a list when appending filenames for export in the top-level
         directory. Files can also be appended to a field to indicate which
         subdirectory they should be exported to. For example, to export
         ``foo.h`` to the top-level directory, and ``bar.h`` to ``mozilla/dom/``,
         append to ``EXPORTS`` like so::
 
            EXPORTS += ['foo.h']
            EXPORTS.mozilla.dom += ['bar.h']
+
+        Entries in ``EXPORTS`` are paths, so objdir paths may be used, but
+        any files listed from the objdir must also be listed in
+        ``GENERATED_FILES``.
         """, None),
 
     'PROGRAM' : (unicode, unicode,
         """Compiled executable name.
 
         If the configuration token ``BIN_SUFFIX`` is set, its value will be
         automatically appended to ``PROGRAM``. If ``PROGRAM`` already ends with
         ``BIN_SUFFIX``, ``PROGRAM`` will remain unchanged.
--- a/python/mozbuild/mozbuild/frontend/data.py
+++ b/python/mozbuild/mozbuild/frontend/data.py
@@ -189,31 +189,16 @@ class BaseDefines(ContextDerived):
             self.defines.update(more_defines)
 
 class Defines(BaseDefines):
     pass
 
 class HostDefines(BaseDefines):
     pass
 
-class Exports(ContextDerived):
-    """Context derived container object for EXPORTS, which is a
-    HierarchicalStringList.
-
-    We need an object derived from ContextDerived for use in the backend, so
-    this object fills that role. It just has a reference to the underlying
-    HierarchicalStringList, which is created when parsing EXPORTS.
-    """
-    __slots__ = ('exports', 'dist_install')
-
-    def __init__(self, context, exports, dist_install=True):
-        ContextDerived.__init__(self, context)
-        self.exports = exports
-        self.dist_install = dist_install
-
 class TestHarnessFiles(ContextDerived):
     """Sandbox container object for TEST_HARNESS_FILES,
     which is a HierarchicalStringList.
 
     We need an object derived from ContextDerived for use in the backend, so
     this object fills that role. It just has a reference to the underlying
     HierarchicalStringList, which is created when parsing TEST_HARNESS_FILES.
     """
@@ -827,16 +812,29 @@ class FinalTargetPreprocessedFiles(Conte
 
 
 class TestingFiles(FinalTargetFiles):
     @property
     def install_target(self):
         return '_tests'
 
 
+class Exports(FinalTargetFiles):
+    """Context derived container object for EXPORTS, which is a
+    HierarchicalStringList.
+
+    We need an object derived from ContextDerived for use in the backend, so
+    this object fills that role. It just has a reference to the underlying
+    HierarchicalStringList, which is created when parsing EXPORTS.
+    """
+    @property
+    def install_target(self):
+        return 'dist/include'
+
+
 class GeneratedFile(ContextDerived):
     """Represents a generated file."""
 
     __slots__ = (
         'script',
         'method',
         'output',
         'inputs',
--- a/python/mozbuild/mozbuild/frontend/emitter.py
+++ b/python/mozbuild/mozbuild/frontend/emitter.py
@@ -603,22 +603,19 @@ class TreeMetadataEmitter(LoggingMixin):
         if dist_install is True:
             passthru.variables['DIST_INSTALL'] = True
         elif dist_install is False:
             passthru.variables['NO_DIST_INSTALL'] = True
 
         for obj in self._process_sources(context, passthru):
             yield obj
 
-        exports = context.get('EXPORTS')
-        if exports:
-            yield Exports(context, exports,
-                dist_install=dist_install is not False)
-
+        generated_files = set()
         for obj in self._process_generated_files(context):
+            generated_files.add(obj.output)
             yield obj
 
         for obj in self._process_test_harness_files(context):
             yield obj
 
         defines = context.get('DEFINES')
         if defines:
             yield Defines(context, defines)
@@ -648,16 +645,17 @@ class TreeMetadataEmitter(LoggingMixin):
                     not os.path.exists(local_include.full_path)):
                 raise SandboxValidationError('Path specified in LOCAL_INCLUDES '
                     'does not exist: %s (resolved to %s)' % (local_include,
                     local_include.full_path), context)
             yield LocalInclude(context, local_include)
 
         components = []
         for var, cls in (
+            ('EXPORTS', Exports),
             ('FINAL_TARGET_FILES', FinalTargetFiles),
             ('FINAL_TARGET_PP_FILES', FinalTargetPreprocessedFiles),
             ('TESTING_FILES', TestingFiles),
         ):
             all_files = context.get(var)
             if not all_files:
                 continue
             if dist_install is False and var != 'TESTING_FILES':
@@ -669,21 +667,33 @@ class TreeMetadataEmitter(LoggingMixin):
             for base, files in all_files.walk():
                 if base == 'components':
                     components.extend(files)
                 if base == 'defaults/pref':
                     has_prefs = True
                 if mozpath.split(base)[0] == 'res':
                     has_resources = True
                 for f in files:
-                    path = f.full_path
-                    if not os.path.exists(path):
+                    if (var == 'FINAL_TARGET_PP_FILES' and
+                        not isinstance(f, SourcePath)):
                         raise SandboxValidationError(
-                            'File listed in %s does not exist: %s'
-                            % (var, path), context)
+                                ('Only source directory paths allowed in ' +
+                                'FINAL_TARGET_PP_FILES: %s')
+                                % (f,), context)
+                    if not isinstance(f, ObjDirPath):
+                        path = f.full_path
+                        if not os.path.exists(path):
+                            raise SandboxValidationError(
+                                'File listed in %s does not exist: %s'
+                                % (var, path), context)
+                    else:
+                        if mozpath.basename(f.full_path) not in generated_files:
+                            raise SandboxValidationError(
+                                ('Objdir file listed in %s not in ' +
+                                 'GENERATED_FILES: %s') % (var, path), context)
 
             # Addons (when XPI_NAME is defined) and Applications (when
             # DIST_SUBDIR is defined) use a different preferences directory
             # (default/preferences) from the one the GRE uses (defaults/pref).
             # Hence, we move the files from the latter to the former in that
             # case.
             if has_prefs and (context.get('XPI_NAME') or
                               context.get('DIST_SUBDIR')):
new file mode 100644
new file mode 100644
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/exports-generated/moz.build
@@ -0,0 +1,12 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+EXPORTS += ['!bar.h', 'foo.h']
+EXPORTS.mozilla += ['!mozilla2.h', 'mozilla1.h']
+EXPORTS.mozilla.dom += ['!dom2.h', '!dom3.h', 'dom1.h']
+EXPORTS.gfx += ['gfx.h']
+
+GENERATED_FILES += ['bar.h']
+GENERATED_FILES += ['mozilla2.h']
+GENERATED_FILES += ['dom2.h']
+GENERATED_FILES += ['dom3.h']
new file mode 100644
--- a/python/mozbuild/mozbuild/test/backend/test_recursivemake.py
+++ b/python/mozbuild/mozbuild/test/backend/test_recursivemake.py
@@ -388,16 +388,59 @@ class TestRecursiveMakeBackend(BackendTe
             '',
             'GENERATED_FILES += quux.c',
             'EXTRA_MDDEPEND_FILES += quux.c.pp',
         ]
 
         self.maxDiff = None
         self.assertEqual(lines, expected)
 
+    def test_exports_generated(self):
+        """Ensure EXPORTS that are listed in GENERATED_FILES
+        are handled properly."""
+        env = self._consume('exports-generated', RecursiveMakeBackend)
+
+        # EXPORTS files should appear in the dist_include install manifest.
+        m = InstallManifest(path=mozpath.join(env.topobjdir,
+            '_build_manifests', 'install', 'dist_include'))
+        self.assertEqual(len(m), 4)
+        self.assertIn('foo.h', m)
+        self.assertIn('mozilla/mozilla1.h', m)
+        self.assertIn('mozilla/dom/dom1.h', m)
+        self.assertIn('gfx/gfx.h', m)
+        # EXPORTS files that are also GENERATED_FILES should be handled as
+        # INSTALL_TARGETS.
+        backend_path = mozpath.join(env.topobjdir, 'backend.mk')
+        lines = [l.strip() for l in open(backend_path, 'rt').readlines()[2:]]
+        expected = [
+            'GENERATED_FILES += bar.h',
+            'EXTRA_MDDEPEND_FILES += bar.h.pp',
+            'GENERATED_FILES += mozilla2.h',
+            'EXTRA_MDDEPEND_FILES += mozilla2.h.pp',
+            'GENERATED_FILES += dom2.h',
+            'EXTRA_MDDEPEND_FILES += dom2.h.pp',
+            'GENERATED_FILES += dom3.h',
+            'EXTRA_MDDEPEND_FILES += dom3.h.pp',
+            'dist_include_FILES += bar.h',
+            'dist_include_DEST := $(DEPTH)/dist/include/',
+            'dist_include_TARGET := export',
+            'INSTALL_TARGETS += dist_include',
+            'dist_include_mozilla_FILES += mozilla2.h',
+            'dist_include_mozilla_DEST := $(DEPTH)/dist/include/mozilla',
+            'dist_include_mozilla_TARGET := export',
+            'INSTALL_TARGETS += dist_include_mozilla',
+            'dist_include_mozilla_dom_FILES += dom2.h',
+            'dist_include_mozilla_dom_FILES += dom3.h',
+            'dist_include_mozilla_dom_DEST := $(DEPTH)/dist/include/mozilla/dom',
+            'dist_include_mozilla_dom_TARGET := export',
+            'INSTALL_TARGETS += dist_include_mozilla_dom',
+        ]
+        self.maxDiff = None
+        self.assertEqual(lines, expected)
+
     def test_resources(self):
         """Ensure RESOURCE_FILES is handled properly."""
         env = self._consume('resources', RecursiveMakeBackend)
 
         # RESOURCE_FILES should appear in the dist_bin install manifest.
         m = InstallManifest(path=os.path.join(env.topobjdir,
             '_build_manifests', 'install', 'dist_bin'))
         self.assertEqual(len(m), 10)
@@ -449,17 +492,17 @@ class TestRecursiveMakeBackend(BackendTe
             self.assertIn('dir1/test_bar.js', o)
 
             self.assertEqual(len(o['xpcshell.js']), 1)
 
     def test_test_manifest_pattern_matches_recorded(self):
         """Pattern matches in test manifests' support-files should be recorded."""
         env = self._consume('test-manifests-written', RecursiveMakeBackend)
         m = InstallManifest(path=mozpath.join(env.topobjdir,
-            '_build_manifests', 'install', 'tests'))
+            '_build_manifests', 'install', '_tests'))
 
         # This is not the most robust test in the world, but it gets the job
         # done.
         entries = [e for e in m._dests.keys() if '**' in e]
         self.assertEqual(len(entries), 1)
         self.assertIn('support/**', entries[0])
 
     def test_xpidl_generation(self):
@@ -695,17 +738,17 @@ class TestRecursiveMakeBackend(BackendTe
 
         self.assertIn('JAR_MANIFEST := %s/jar.mn' % env.topsrcdir, lines)
 
     def test_test_manifests_duplicate_support_files(self):
         """Ensure duplicate support-files in test manifests work."""
         env = self._consume('test-manifests-duplicate-support-files',
             RecursiveMakeBackend)
 
-        p = os.path.join(env.topobjdir, '_build_manifests', 'install', 'tests')
+        p = os.path.join(env.topobjdir, '_build_manifests', 'install', '_tests')
         m = InstallManifest(p)
         self.assertIn('testing/mochitest/tests/support-file.txt', m)
 
     def test_android_eclipse(self):
         env = self._consume('android_eclipse', RecursiveMakeBackend)
 
         with open(mozpath.join(env.topobjdir, 'backend.mk'), 'rb') as fh:
             lines = fh.readlines()
@@ -750,17 +793,17 @@ class TestRecursiveMakeBackend(BackendTe
             o = json.load(fh)
 
             self.assertIn('mochitest.js', o)
             self.assertIn('not_packaged.java', o)
 
         man_dir = mozpath.join(env.topobjdir, '_build_manifests', 'install')
         self.assertTrue(os.path.isdir(man_dir))
 
-        full = mozpath.join(man_dir, 'tests')
+        full = mozpath.join(man_dir, '_tests')
         self.assertTrue(os.path.exists(full))
 
         m = InstallManifest(path=full)
 
         # Only mochitest.js should be in the install manifest.
         self.assertTrue('testing/mochitest/tests/mochitest.js' in m)
 
         # The path is odd here because we do not normalize at test manifest
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/frontend/data/exports-generated/moz.build
@@ -0,0 +1,8 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+EXPORTS += ['foo.h']
+EXPORTS.mozilla += ['mozilla1.h']
+EXPORTS.mozilla += ['!mozilla2.h']
+
+GENERATED_FILES += ['mozilla2.h']
new file mode 100644
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/frontend/data/exports-missing-generated/moz.build
@@ -0,0 +1,5 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+EXPORTS += ['foo.h']
+EXPORTS += ['!bar.h']
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/frontend/data/exports-missing/moz.build
@@ -0,0 +1,6 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+EXPORTS += ['foo.h']
+EXPORTS.mozilla += ['mozilla1.h']
+EXPORTS.mozilla += ['mozilla2.h']
new file mode 100644
new file mode 100644
new file mode 100644
new file mode 100644
new file mode 100644
new file mode 100644
new file mode 100644
new file mode 100644
new file mode 100644
new file mode 100644
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/frontend/data/final-target-pp-files-non-srcdir/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+FINAL_TARGET_PP_FILES += [
+    '!foo.js',
+]
--- a/python/mozbuild/mozbuild/test/frontend/test_emitter.py
+++ b/python/mozbuild/mozbuild/test/frontend/test_emitter.py
@@ -4,16 +4,19 @@
 
 from __future__ import unicode_literals
 
 import os
 import unittest
 
 from mozunit import main
 
+from mozbuild.frontend.context import (
+    ObjDirPath,
+)
 from mozbuild.frontend.data import (
     AndroidResDirs,
     BrandingFiles,
     ChromeManifestEntry,
     ConfigFileSubstitution,
     Defines,
     DirectoryTraversal,
     Exports,
@@ -249,20 +252,52 @@ class TestEmitterBasic(unittest.TestCase
             ('', ['foo.h', 'bar.h', 'baz.h']),
             ('mozilla', ['mozilla1.h', 'mozilla2.h']),
             ('mozilla/dom', ['dom1.h', 'dom2.h', 'dom3.h']),
             ('mozilla/gfx', ['gfx.h']),
             ('nspr/private', ['pprio.h', 'pprthred.h']),
             ('vpx', ['mem.h', 'mem2.h']),
         ]
         for (expect_path, expect_headers), (actual_path, actual_headers) in \
-                zip(expected, [(path, list(seq)) for path, seq in objs[0].exports.walk()]):
+                zip(expected, [(path, list(seq)) for path, seq in objs[0].files.walk()]):
             self.assertEqual(expect_path, actual_path)
             self.assertEqual(expect_headers, actual_headers)
 
+    def test_exports_missing(self):
+        '''
+        Missing files in EXPORTS is an error.
+        '''
+        reader = self.reader('exports-missing')
+        with self.assertRaisesRegexp(SandboxValidationError,
+             'File listed in EXPORTS does not exist:'):
+            objs = self.read_topsrcdir(reader)
+
+    def test_exports_missing_generated(self):
+        '''
+        An objdir file in EXPORTS that is not in GENERATED_FILES is an error.
+        '''
+        reader = self.reader('exports-missing-generated')
+        with self.assertRaisesRegexp(SandboxValidationError,
+             'Objdir file listed in EXPORTS not in GENERATED_FILES:'):
+            objs = self.read_topsrcdir(reader)
+
+    def test_exports_generated(self):
+        reader = self.reader('exports-generated')
+        objs = self.read_topsrcdir(reader)
+
+        self.assertEqual(len(objs), 2)
+        self.assertIsInstance(objs[0], GeneratedFile)
+        self.assertIsInstance(objs[1], Exports)
+        exports = [(path, list(seq)) for path, seq in objs[1].files.walk()]
+        self.assertEqual(exports,
+                         [('', ['foo.h']),
+                          ('mozilla', ['mozilla1.h', '!mozilla2.h'])])
+        path, files = exports[1]
+        self.assertIsInstance(files[1], ObjDirPath)
+
     def test_test_harness_files(self):
         reader = self.reader('test-harness-files')
         objs = self.read_topsrcdir(reader)
 
         self.assertEqual(len(objs), 1)
         self.assertIsInstance(objs[0], TestHarnessFiles)
 
         expected = {
@@ -829,16 +864,23 @@ class TestEmitterBasic(unittest.TestCase
 
     def test_missing_final_target_pp_files(self):
         """Test that FINAL_TARGET_PP_FILES with missing files throws errors."""
         with self.assertRaisesRegexp(SandboxValidationError, 'File listed in '
             'FINAL_TARGET_PP_FILES does not exist'):
             reader = self.reader('dist-files-missing')
             self.read_topsrcdir(reader)
 
+    def test_final_target_pp_files_non_srcdir(self):
+        '''Test that non-srcdir paths in FINAL_TARGET_PP_FILES throws errors.'''
+        reader = self.reader('final-target-pp-files-non-srcdir')
+        with self.assertRaisesRegexp(SandboxValidationError,
+             'Only source directory paths allowed in FINAL_TARGET_PP_FILES:'):
+            objs = self.read_topsrcdir(reader)
+
     def test_android_res_dirs(self):
         """Test that ANDROID_RES_DIRS works properly."""
         reader = self.reader('android-res-dirs')
         objs = self.read_topsrcdir(reader)
 
         self.assertEqual(len(objs), 1)
         self.assertIsInstance(objs[0], AndroidResDirs)