Bug 835309 - Look at .xpi file contents when unifying them for universal builds. r=gps
authorMike Hommey <mh+mozilla@glandium.org>
Sun, 03 Feb 2013 07:19:15 +0100
changeset 120606 d949c8ee39da1edc021415377c4a4fc74caa530e
parent 120605 6474019340d3fd5e31cbf1f5c7990fd03dd8a810
child 120607 5f21061637d59d26776fa8cb8cb975326bb50a1a
push id24259
push usermh@glandium.org
push dateSun, 03 Feb 2013 06:23:08 +0000
treeherdermozilla-central@ba515e203813 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs835309
milestone21.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 835309 - Look at .xpi file contents when unifying them for universal builds. r=gps
python/mozbuild/mozpack/errors.py
python/mozbuild/mozpack/files.py
python/mozbuild/mozpack/test/test_copier.py
python/mozbuild/mozpack/test/test_files.py
python/mozbuild/mozpack/test/test_unify.py
python/mozbuild/mozpack/unify.py
toolkit/mozapps/installer/packager.py
--- a/python/mozbuild/mozpack/errors.py
+++ b/python/mozbuild/mozpack/errors.py
@@ -123,10 +123,15 @@ class ErrorCollector(object):
         assert self._count is None
         self._count = 0
         yield
         count = self._count
         self._count = None
         if count:
             raise AccumulatedErrors()
 
+    @property
+    def count(self):
+        # _count can be None.
+        return self._count if self._count else 0
+
 
 errors = ErrorCollector()
--- a/python/mozbuild/mozpack/files.py
+++ b/python/mozbuild/mozpack/files.py
@@ -10,17 +10,19 @@ from mozpack.executables import (
     may_strip,
     strip,
     may_elfhack,
     elfhack,
 )
 from mozpack.chrome.manifest import ManifestEntry
 from io import BytesIO
 from mozpack.errors import ErrorMessage
+from mozpack.mozjar import JarReader
 import mozpack.path
+from collections import OrderedDict
 
 
 class Dest(object):
     '''
     Helper interface for BaseFile.copy. The interface works as follows:
     - read() and write() can be used to sequentially read/write from the
       underlying file.
     - a call to read() after a write() will re-open the underlying file and
@@ -316,46 +318,90 @@ class MinifiedProperties(BaseFile):
         '''
         Return a file-like object allowing to read() the minified content of
         the properties file.
         '''
         return BytesIO(''.join(l for l in self._file.open().readlines()
                                if not l.startswith('#')))
 
 
-class FileFinder(object):
-    '''
-    Helper to get appropriate BaseFile instances from the file system.
-    '''
+class BaseFinder(object):
     def __init__(self, base, minify=False):
         '''
-        Create a FileFinder for files under the given base directory. The
+        Initializes the instance with a reference base directory. The
         optional minify argument specifies whether file types supporting
         minification (currently only "*.properties") should be minified.
         '''
         self.base = base
         self._minify = minify
 
     def find(self, pattern):
         '''
         Yield path, BaseFile_instance pairs for all files under the base
         directory and its subdirectories that match the given pattern. See the
         mozpack.path.match documentation for a description of the handled
-        patterns. Note all files with a name starting with a '.' are ignored
-        when scanning directories, but are not ignored when explicitely
-        requested.
+        patterns.
         '''
         while pattern.startswith('/'):
             pattern = pattern[1:]
-        return self._find(pattern)
+        for p, f in self._find(pattern):
+            yield p, self._minify_file(p, f)
+
+    def __iter__(self):
+        '''
+        Iterates over all files under the base directory (excluding files
+        starting with a '.' and files at any level under a directory starting
+        with a '.').
+            for path, file in finder:
+                ...
+        '''
+        return self.find('')
+
+    def __contains__(self, pattern):
+        raise RuntimeError("'in' operator forbidden for %s. Use contains()." %
+                           self.__class__.__name__)
+
+    def contains(self, pattern):
+        '''
+        Return whether some files under the base directory match the given
+        pattern. See the mozpack.path.match documentation for a description of
+        the handled patterns.
+        '''
+        return any(self.find(pattern))
+
+    def _minify_file(self, path, file):
+        '''
+        Return an appropriate MinifiedSomething wrapper for the given BaseFile
+        instance (file), according to the file type (determined by the given
+        path), if the FileFinder was created with minification enabled.
+        Otherwise, just return the given BaseFile instance.
+        Currently, only "*.properties" files are handled.
+        '''
+        if self._minify and not isinstance(file, ExecutableFile):
+            if path.endswith('.properties'):
+                return MinifiedProperties(file)
+        return file
+
+
+class FileFinder(BaseFinder):
+    '''
+    Helper to get appropriate BaseFile instances from the file system.
+    '''
+    def __init__(self, base, **kargs):
+        '''
+        Create a FileFinder for files under the given base directory.
+        '''
+        BaseFinder.__init__(self, base, **kargs)
 
     def _find(self, pattern):
         '''
         Actual implementation of FileFinder.find(), dispatching to specialized
         member functions depending on what kind of pattern was given.
+        Note all files with a name starting with a '.' are ignored when
+        scanning directories, but are not ignored when explicitely requested.
         '''
         if '*' in pattern:
             return self._find_glob('', mozpack.path.split(pattern))
         elif os.path.isdir(os.path.join(self.base, pattern)):
             return self._find_dir(pattern)
         else:
             return self._find_file(pattern)
 
@@ -379,17 +425,17 @@ class FileFinder(object):
         '''
         srcpath = os.path.join(self.base, path)
         if not os.path.exists(srcpath):
             return
 
         if is_executable(srcpath):
             yield path, ExecutableFile(srcpath)
         else:
-            yield path, self._minify_file(srcpath, File(srcpath))
+            yield path, File(srcpath)
 
     def _find_glob(self, base, pattern):
         '''
         Actual implementation of FileFinder.find() when the given pattern
         contains globbing patterns ('*' or '**'). This is meant to be an
         equivalent of:
             for p, f in self:
                 if mozpack.path.match(p, pattern):
@@ -413,42 +459,40 @@ class FileFinder(object):
                     for p_, f in self._find_glob(mozpack.path.join(base, p),
                                                  pattern[1:]):
                         yield p_, f
         else:
             for p, f in self._find_glob(mozpack.path.join(base, pattern[0]),
                                         pattern[1:]):
                 yield p, f
 
-    def __iter__(self):
-        '''
-        Iterates over all files under the base directory (excluding files
-        starting with a '.' and files at any level under a directory starting
-        with a '.').
-            for path, file in finder:
-                ...
+
+class JarFinder(BaseFinder):
+    '''
+    Helper to get appropriate DeflatedFile instances from a JarReader.
+    '''
+    def __init__(self, base, reader, **kargs):
         '''
-        return self.find('')
+        Create a JarFinder for files in the given JarReader. The base argument
+        is used as an indication of the Jar file location.
+        '''
+        assert isinstance(reader, JarReader)
+        BaseFinder.__init__(self, base, **kargs)
+        self._files = OrderedDict((f.filename, f) for f in reader)
 
-    def __contains__(self, pattern):
-        raise RuntimeError("'in' operator forbidden for %s. Use contains()." %
-                           self.__class__.__name__)
-
-    def contains(self, pattern):
+    def _find(self, pattern):
         '''
-        Return whether some files under the base directory match the given
-        pattern. See the mozpack.path.match documentation for a description of
-        the handled patterns.
-        '''
-        return any(self.find(pattern))
-
-    def _minify_file(self, path, file):
+        Actual implementation of JarFinder.find(), dispatching to specialized
+        member functions depending on what kind of pattern was given.
         '''
-        Return an appropriate MinifiedSomething wrapper for the given BaseFile
-        instance (file), according to the file type (determined by the given
-        path), if the FileFinder was created with minification enabled.
-        Otherwise, just return the given BaseFile instance.
-        Currently, only "*.properties" files are handled.
-        '''
-        if self._minify:
-            if path.endswith('.properties'):
-                return MinifiedProperties(file)
-        return file
+        if '*' in pattern:
+            for p in self._files:
+                if mozpack.path.match(p, pattern):
+                    yield p, DeflatedFile(self._files[p])
+        elif pattern == '':
+            for p in self._files:
+                yield p, DeflatedFile(self._files[p])
+        elif pattern in self._files:
+            yield pattern, DeflatedFile(self._files[pattern])
+        else:
+            for p in self._files:
+                if mozpack.path.basedir(p, [pattern]) == pattern:
+                    yield p, DeflatedFile(self._files[p])
--- a/python/mozbuild/mozpack/test/test_copier.py
+++ b/python/mozbuild/mozpack/test/test_copier.py
@@ -54,16 +54,17 @@ class TestFileRegistry(MatchTestTemplate
 
         self.assertEqual(self.registry.paths(), ['foo', 'bar'])
 
         self.registry.remove('foo')
         self.assertEqual(self.registry.paths(), ['bar'])
         self.registry.remove('bar')
         self.assertEqual(self.registry.paths(), [])
 
+        self.prepare_match_test()
         self.do_match_test()
         self.assertTrue(self.checked)
         self.assertEqual(self.registry.paths(), [
             'bar',
             'foo/bar',
             'foo/baz',
             'foo/qux/1',
             'foo/qux/bar',
--- a/python/mozbuild/mozpack/test/test_files.py
+++ b/python/mozbuild/mozpack/test/test_files.py
@@ -6,16 +6,17 @@ from mozpack.files import (
     Dest,
     File,
     GeneratedFile,
     DeflatedFile,
     ManifestFile,
     XPTFile,
     MinifiedProperties,
     FileFinder,
+    JarFinder,
 )
 from mozpack.mozjar import (
     JarReader,
     JarWriter,
 )
 from mozpack.chrome.manifest import (
     ManifestContent,
     ManifestResource,
@@ -481,25 +482,29 @@ class TestMinifiedProperties(TestWithTmp
         open(self.tmppath('prop'), 'wb').write('\n'.join(propLines))
         MinifiedProperties(File(self.tmppath('prop'))) \
             .copy(self.tmppath('prop2'))
         self.assertEqual(open(self.tmppath('prop2')).readlines(),
                          ['foo = bar\n', '\n'])
 
 
 class MatchTestTemplate(object):
-    def do_match_test(self):
+    def prepare_match_test(self, with_dotfiles=False):
         self.add('bar')
         self.add('foo/bar')
         self.add('foo/baz')
         self.add('foo/qux/1')
         self.add('foo/qux/bar')
         self.add('foo/qux/2/test')
         self.add('foo/qux/2/test2')
+        if with_dotfiles:
+            self.add('foo/.foo')
+            self.add('foo/.bar/foo')
 
+    def do_match_test(self):
         self.do_check('', [
             'bar', 'foo/bar', 'foo/baz', 'foo/qux/1', 'foo/qux/bar',
             'foo/qux/2/test', 'foo/qux/2/test2'
         ])
         self.do_check('*', [
             'bar', 'foo/bar', 'foo/baz', 'foo/qux/1', 'foo/qux/bar',
             'foo/qux/2/test', 'foo/qux/2/test2'
         ])
@@ -528,46 +533,69 @@ class MatchTestTemplate(object):
         self.do_check('**/2/test*', ['foo/qux/2/test', 'foo/qux/2/test2'])
         self.do_check('**/foo', [
             'foo/bar', 'foo/baz', 'foo/qux/1', 'foo/qux/bar',
             'foo/qux/2/test', 'foo/qux/2/test2'
         ])
         self.do_check('**/barbaz', [])
         self.do_check('f**/bar', ['foo/bar'])
 
+    def do_finder_test(self, finder):
+        self.assertTrue(finder.contains('foo/.foo'))
+        self.assertTrue(finder.contains('foo/.bar'))
+        self.assertTrue('foo/.foo' in [f for f, c in
+                                       finder.find('foo/.foo')])
+        self.assertTrue('foo/.bar/foo' in [f for f, c in
+                                           finder.find('foo/.bar')])
+        self.assertEqual(sorted([f for f, c in finder.find('foo/.*')]),
+                         ['foo/.bar/foo', 'foo/.foo'])
+        for pattern in ['foo', '**', '**/*', '**/foo', 'foo/*']:
+            self.assertFalse('foo/.foo' in [f for f, c in
+                                            finder.find(pattern)])
+            self.assertFalse('foo/.bar/foo' in [f for f, c in
+                                                finder.find(pattern)])
+            self.assertEqual(sorted([f for f, c in finder.find(pattern)]),
+                             sorted([f for f, c in finder
+                                     if mozpack.path.match(f, pattern)]))
+
+
+def do_check(test, finder, pattern, result):
+    if result:
+        test.assertTrue(finder.contains(pattern))
+    else:
+        test.assertFalse(finder.contains(pattern))
+    test.assertEqual(sorted(list(f for f, c in finder.find(pattern))),
+                     sorted(result))
+
 
 class TestFileFinder(MatchTestTemplate, TestWithTmpDir):
     def add(self, path):
         ensure_parent_dir(self.tmppath(path))
         open(self.tmppath(path), 'wb').write(path)
 
     def do_check(self, pattern, result):
-        if result:
-            self.assertTrue(self.finder.contains(pattern))
-        else:
-            self.assertFalse(self.finder.contains(pattern))
-        self.assertEqual(sorted(list(f for f, c in self.finder.find(pattern))),
-                         sorted(result))
+        do_check(self, self.finder, pattern, result)
 
     def test_file_finder(self):
+        self.prepare_match_test(with_dotfiles=True)
         self.finder = FileFinder(self.tmpdir)
         self.do_match_test()
-        self.add('foo/.foo')
-        self.add('foo/.bar/foo')
-        self.assertTrue(self.finder.contains('foo/.foo'))
-        self.assertTrue(self.finder.contains('foo/.bar'))
-        self.assertTrue('foo/.foo' in [f for f, c in
-                                       self.finder.find('foo/.foo')])
-        self.assertTrue('foo/.bar/foo' in [f for f, c in
-                                           self.finder.find('foo/.bar')])
-        self.assertEqual(sorted([f for f, c in self.finder.find('foo/.*')]),
-                         ['foo/.bar/foo', 'foo/.foo'])
-        for pattern in ['foo', '**', '**/*', '**/foo', 'foo/*']:
-            self.assertFalse('foo/.foo' in [f for f, c in
-                                            self.finder.find(pattern)])
-            self.assertFalse('foo/.bar/foo' in [f for f, c in
-                                                self.finder.find(pattern)])
-            self.assertEqual(sorted([f for f, c in self.finder.find(pattern)]),
-                             sorted([f for f, c in self.finder
-                                     if mozpack.path.match(f, pattern)]))
+        self.do_finder_test(self.finder)
+
+
+class TestJarFinder(MatchTestTemplate, TestWithTmpDir):
+    def add(self, path):
+        self.jar.add(path, path, compress=True)
+
+    def do_check(self, pattern, result):
+        do_check(self, self.finder, pattern, result)
+
+    def test_jar_finder(self):
+        self.jar = JarWriter(file=self.tmppath('test.jar'))
+        self.prepare_match_test()
+        self.jar.finish()
+        reader = JarReader(file=self.tmppath('test.jar'))
+        self.finder = JarFinder(self.tmppath('test.jar'), reader)
+        self.do_match_test()
+
 
 if __name__ == '__main__':
     mozunit.main()
--- a/python/mozbuild/mozpack/test/test_unify.py
+++ b/python/mozbuild/mozpack/test/test_unify.py
@@ -4,18 +4,27 @@
 
 from mozpack.unify import (
     UnifiedFinder,
     UnifiedBuildFinder,
 )
 import mozunit
 from mozpack.test.test_files import TestWithTmpDir
 from mozpack.copier import ensure_parent_dir
+from mozpack.files import FileFinder
+from mozpack.mozjar import JarWriter
+from mozpack.test.test_files import MockDest
+from cStringIO import StringIO
 import os
-from mozpack.errors import ErrorMessage
+import sys
+from mozpack.errors import (
+    ErrorMessage,
+    AccumulatedErrors,
+    errors,
+)
 
 
 class TestUnified(TestWithTmpDir):
     def create_one(self, which, path, content):
         file = self.tmppath(os.path.join(which, path))
         ensure_parent_dir(file)
         open(file, 'wb').write(content)
 
@@ -31,17 +40,18 @@ class TestUnifiedFinder(TestUnified):
         self.create_one('a', 'bar', 'bar')
         self.create_one('b', 'baz', 'baz')
         self.create_one('a', 'qux', 'foobar')
         self.create_one('b', 'qux', 'baz')
         self.create_one('a', 'test/foo', 'a\nb\nc\n')
         self.create_one('b', 'test/foo', 'b\nc\na\n')
         self.create_both('test/bar', 'a\nb\nc\n')
 
-        finder = UnifiedFinder(self.tmppath('a'), self.tmppath('b'),
+        finder = UnifiedFinder(FileFinder(self.tmppath('a')),
+                               FileFinder(self.tmppath('b')),
                                sorted=['test'])
         self.assertEqual(sorted([(f, c.open().read())
                                  for f, c in finder.find('foo')]),
                          [('foo/bar', 'foobar'), ('foo/baz', 'foobaz')])
         self.assertRaises(ErrorMessage, any, finder.find('bar'))
         self.assertRaises(ErrorMessage, any, finder.find('baz'))
         self.assertRaises(ErrorMessage, any, finder.find('qux'))
         self.assertEqual(sorted([(f, c.open().read())
@@ -68,17 +78,18 @@ class TestUnifiedBuildFinder(TestUnified
                         '\n'.join([
                             '<html>',
                             '<body>',
                             '<h1>about:buildconfig</h1>',
                             '<div>bar</div>',
                             '</body>',
                             '</html>',
                         ]))
-        finder = UnifiedBuildFinder(self.tmppath('a'), self.tmppath('b'))
+        finder = UnifiedBuildFinder(FileFinder(self.tmppath('a')),
+                                    FileFinder(self.tmppath('b')))
         self.assertEqual(sorted([(f, c.open().read()) for f, c in
                                  finder.find('**/chrome.manifest')]),
                          [('chrome.manifest', 'a\nb\nc\n'),
                           ('chrome/chrome.manifest', 'a\nb\nc\n')])
 
         self.assertEqual(sorted([(f, c.open().read()) for f, c in
                                  finder.find('**/buildconfig.html')]),
                          [('chrome/browser/foo/buildconfig.html', '\n'.join([
@@ -87,11 +98,30 @@ class TestUnifiedBuildFinder(TestUnified
                              '<h1>about:buildconfig</h1>',
                              '<div>foo</div>',
                              '<hr> </hr>',
                              '<div>bar</div>',
                              '</body>',
                              '</html>',
                          ]))])
 
+        xpi = MockDest()
+        with JarWriter(fileobj=xpi, compress=True) as jar:
+            jar.add('foo', 'foo')
+            jar.add('bar', 'bar')
+        foo_xpi = xpi.read()
+        self.create_both('foo.xpi', foo_xpi)
+
+        with JarWriter(fileobj=xpi, compress=True) as jar:
+            jar.add('foo', 'bar')
+        self.create_one('a', 'bar.xpi', foo_xpi)
+        self.create_one('b', 'bar.xpi', xpi.read())
+
+        errors.out = StringIO()
+        with self.assertRaises(AccumulatedErrors), errors.accumulate():
+            self.assertEqual([(f, c.open().read()) for f, c in
+                              finder.find('*.xpi')],
+                             [('foo.xpi', foo_xpi)])
+        errors.out = sys.stderr
+
 
 if __name__ == '__main__':
     mozunit.main()
--- a/python/mozbuild/mozpack/unify.py
+++ b/python/mozbuild/mozpack/unify.py
@@ -1,23 +1,25 @@
 # 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 mozpack.files import (
-    FileFinder,
+    BaseFinder,
+    JarFinder,
     ExecutableFile,
     BaseFile,
     GeneratedFile,
 )
 from mozpack.executables import (
     MACHO_SIGNATURES,
     may_strip,
     strip,
 )
+from mozpack.mozjar import JarReader
 from mozpack.errors import errors
 from tempfile import mkstemp
 import mozpack.path
 import shutil
 import struct
 import os
 import subprocess
 from collections import OrderedDict
@@ -62,76 +64,80 @@ class UnifiedExecutableFile(BaseFile):
                 if may_strip(f):
                     strip(f)
             subprocess.call(['lipo', '-create'] + tmpfiles + ['-output', dest])
         finally:
             for f in tmpfiles:
                 os.unlink(f)
 
 
-class UnifiedFinder(FileFinder):
+class UnifiedFinder(BaseFinder):
     '''
     Helper to get unified BaseFile instances from two distinct trees on the
     file system.
     '''
-    def __init__(self, base1, base2, sorted=[], **kargs):
+    def __init__(self, finder1, finder2, sorted=[], **kargs):
         '''
-        Initialize a UnifiedFinder. base1 and base2 are the base directories
-        for the two trees from which files are picked. UnifiedFinder.find()
-        will act as FileFinder.find() but will error out when matches can only
-        be found in one of the two trees and not the other. It will also error
-        out if matches can be found on both ends but their contents are not
-        identical.
+        Initialize a UnifiedFinder. finder1 and finder2 are BaseFinder
+        instances from which files are picked. UnifiedFinder.find() will act as
+        FileFinder.find() but will error out when matches can only be found in
+        one of the two trees and not the other. It will also error out if
+        matches can be found on both ends but their contents are not identical.
 
         The sorted argument gives a list of mozpack.path.match patterns. File
         paths matching one of these patterns will have their contents compared
         with their lines sorted.
         '''
-        self._base1 = FileFinder(base1, **kargs)
-        self._base2 = FileFinder(base2, **kargs)
+        assert isinstance(finder1, BaseFinder)
+        assert isinstance(finder2, BaseFinder)
+        self._finder1 = finder1
+        self._finder2 = finder2
         self._sorted = sorted
+        BaseFinder.__init__(self, finder1.base, **kargs)
 
     def _find(self, path):
         '''
         UnifiedFinder.find() implementation.
         '''
         files1 = OrderedDict()
-        for p, f in self._base1.find(path):
+        for p, f in self._finder1.find(path):
             files1[p] = f
         files2 = set()
-        for p, f in self._base2.find(path):
+        for p, f in self._finder2.find(path):
             files2.add(p)
             if p in files1:
                 if may_unify_binary(files1[p]) and \
                         may_unify_binary(f):
                     yield p, UnifiedExecutableFile(files1[p].path, f.path)
                 else:
+                    err = errors.count
                     unified = self.unify_file(p, files1[p], f)
                     if unified:
                         yield p, unified
-                    else:
+                    elif err == errors.count:
                         self._report_difference(p, files1[p], f)
             else:
-                errors.error('File missing in %s: %s' % (self._base1.base, p))
+                errors.error('File missing in %s: %s' %
+                             (self._finder1.base, p))
         for p in [p for p in files1 if not p in files2]:
-            errors.error('File missing in %s: %s' % (self._base2.base, p))
+            errors.error('File missing in %s: %s' % (self._finder2.base, p))
 
     def _report_difference(self, path, file1, file2):
         '''
         Report differences between files in both trees.
         '''
         errors.error("Can't unify %s: file differs between %s and %s" %
-                     (path, self._base1.base, self._base2.base))
+                     (path, self._finder1.base, self._finder2.base))
         if not isinstance(file1, ExecutableFile) and \
                 not isinstance(file2, ExecutableFile):
             from difflib import unified_diff
             for line in unified_diff(file1.open().readlines(),
                                      file2.open().readlines(),
-                                     os.path.join(self._base1.base, path),
-                                     os.path.join(self._base2.base, path)):
+                                     os.path.join(self._finder1.base, path),
+                                     os.path.join(self._finder2.base, path)):
                 errors.out.write(line)
 
     def unify_file(self, path, file1, file2):
         '''
         Given two BaseFiles and the path they were found at, check whether
         their content match and return the first BaseFile if they do.
         '''
         content1 = file1.open().readlines()
@@ -147,18 +153,18 @@ class UnifiedFinder(FileFinder):
 
 
 class UnifiedBuildFinder(UnifiedFinder):
     '''
     Specialized UnifiedFinder for Mozilla applications packaging. It allows
     "*.manifest" files to differ in their order, and unifies "buildconfig.html"
     files by merging their content.
     '''
-    def __init__(self, base1, base2, **kargs):
-        UnifiedFinder.__init__(self, base1, base2,
+    def __init__(self, finder1, finder2, **kargs):
+        UnifiedFinder.__init__(self, finder1, finder2,
                                sorted=['**/*.manifest'], **kargs)
 
     def unify_file(self, path, file1, file2):
         '''
         Unify buildconfig.html contents, or defer to UnifiedFinder.unify_file.
         '''
         if mozpack.path.basename(path) == 'buildconfig.html':
             content1 = file1.open().readlines()
@@ -166,9 +172,20 @@ class UnifiedBuildFinder(UnifiedFinder):
             # Copy everything from the first file up to the end of its <body>,
             # insert a <hr> between the two files and copy the second file's
             # content beginning after its leading <h1>.
             return GeneratedFile(''.join(
                 content1[:content1.index('</body>\n')] +
                 ['<hr> </hr>\n'] +
                 content2[content2.index('<h1>about:buildconfig</h1>\n') + 1:]
             ))
+        if path.endswith('.xpi'):
+            finder1 = JarFinder(os.path.join(self._finder1.base, path),
+                                JarReader(fileobj=file1.open()))
+            finder2 = JarFinder(os.path.join(self._finder2.base, path),
+                                JarReader(fileobj=file2.open()))
+            unifier = UnifiedFinder(finder1, finder2, sorted=self._sorted)
+            err = errors.count
+            all(unifier.find(''))
+            if err == errors.count:
+                return file1
+            return None
         return UnifiedFinder.unify_file(self, path, file1, file2)
--- a/toolkit/mozapps/installer/packager.py
+++ b/toolkit/mozapps/installer/packager.py
@@ -289,17 +289,18 @@ def main():
                                          key=is_native, reverse=True)
         if is_native(args.source):
             launcher.tooldir = args.source
     elif not buildconfig.substs['CROSS_COMPILE']:
         launcher.tooldir = buildconfig.substs['LIBXUL_DIST']
 
     with errors.accumulate():
         if args.unify:
-            finder = UnifiedBuildFinder(args.source, args.unify,
+            finder = UnifiedBuildFinder(FileFinder(args.source),
+                                        FileFinder(args.unify),
                                         minify=args.minify)
         else:
             finder = FileFinder(args.source, minify=args.minify)
         if 'NO_PKG_FILES' in os.environ:
             sinkformatter = NoPkgFilesRemover(formatter,
                                               args.manifest is not None)
         else:
             sinkformatter = formatter