Bug 835309 - Look at .xpi file contents when unifying them for universal builds. r=gps

This commit is contained in:
Mike Hommey
2013-02-03 07:19:15 +01:00
parent be417d0ed2
commit 0db6eb8e66
7 changed files with 216 additions and 90 deletions

View File

@@ -128,5 +128,10 @@ class ErrorCollector(object):
if count: if count:
raise AccumulatedErrors() raise AccumulatedErrors()
@property
def count(self):
# _count can be None.
return self._count if self._count else 0
errors = ErrorCollector() errors = ErrorCollector()

View File

@@ -15,7 +15,9 @@ from mozpack.executables import (
from mozpack.chrome.manifest import ManifestEntry from mozpack.chrome.manifest import ManifestEntry
from io import BytesIO from io import BytesIO
from mozpack.errors import ErrorMessage from mozpack.errors import ErrorMessage
from mozpack.mozjar import JarReader
import mozpack.path import mozpack.path
from collections import OrderedDict
class Dest(object): class Dest(object):
@@ -321,13 +323,10 @@ class MinifiedProperties(BaseFile):
if not l.startswith('#'))) if not l.startswith('#')))
class FileFinder(object): class BaseFinder(object):
'''
Helper to get appropriate BaseFile instances from the file system.
'''
def __init__(self, base, minify=False): 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 optional minify argument specifies whether file types supporting
minification (currently only "*.properties") should be minified. minification (currently only "*.properties") should be minified.
''' '''
@@ -339,18 +338,65 @@ class FileFinder(object):
Yield path, BaseFile_instance pairs for all files under the base Yield path, BaseFile_instance pairs for all files under the base
directory and its subdirectories that match the given pattern. See the directory and its subdirectories that match the given pattern. See the
mozpack.path.match documentation for a description of the handled mozpack.path.match documentation for a description of the handled
patterns. Note all files with a name starting with a '.' are ignored patterns.
when scanning directories, but are not ignored when explicitely
requested.
''' '''
while pattern.startswith('/'): while pattern.startswith('/'):
pattern = pattern[1:] 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): def _find(self, pattern):
''' '''
Actual implementation of FileFinder.find(), dispatching to specialized Actual implementation of FileFinder.find(), dispatching to specialized
member functions depending on what kind of pattern was given. 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: if '*' in pattern:
return self._find_glob('', mozpack.path.split(pattern)) return self._find_glob('', mozpack.path.split(pattern))
@@ -384,7 +430,7 @@ class FileFinder(object):
if is_executable(srcpath): if is_executable(srcpath):
yield path, ExecutableFile(srcpath) yield path, ExecutableFile(srcpath)
else: else:
yield path, self._minify_file(srcpath, File(srcpath)) yield path, File(srcpath)
def _find_glob(self, base, pattern): def _find_glob(self, base, pattern):
''' '''
@@ -418,37 +464,35 @@ class FileFinder(object):
pattern[1:]): pattern[1:]):
yield p, f 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:
...
'''
return self.find('')
def __contains__(self, pattern): class JarFinder(BaseFinder):
raise RuntimeError("'in' operator forbidden for %s. Use contains()." % '''
self.__class__.__name__) Helper to get appropriate DeflatedFile instances from a JarReader.
'''
def __init__(self, base, reader, **kargs):
'''
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): def _find(self, pattern):
''' '''
Return whether some files under the base directory match the given Actual implementation of JarFinder.find(), dispatching to specialized
pattern. See the mozpack.path.match documentation for a description of member functions depending on what kind of pattern was given.
the handled patterns.
''' '''
return any(self.find(pattern)) if '*' in pattern:
for p in self._files:
def _minify_file(self, path, file): if mozpack.path.match(p, pattern):
''' yield p, DeflatedFile(self._files[p])
Return an appropriate MinifiedSomething wrapper for the given BaseFile elif pattern == '':
instance (file), according to the file type (determined by the given for p in self._files:
path), if the FileFinder was created with minification enabled. yield p, DeflatedFile(self._files[p])
Otherwise, just return the given BaseFile instance. elif pattern in self._files:
Currently, only "*.properties" files are handled. yield pattern, DeflatedFile(self._files[pattern])
''' else:
if self._minify: for p in self._files:
if path.endswith('.properties'): if mozpack.path.basedir(p, [pattern]) == pattern:
return MinifiedProperties(file) yield p, DeflatedFile(self._files[p])
return file

View File

@@ -59,6 +59,7 @@ class TestFileRegistry(MatchTestTemplate, unittest.TestCase):
self.registry.remove('bar') self.registry.remove('bar')
self.assertEqual(self.registry.paths(), []) self.assertEqual(self.registry.paths(), [])
self.prepare_match_test()
self.do_match_test() self.do_match_test()
self.assertTrue(self.checked) self.assertTrue(self.checked)
self.assertEqual(self.registry.paths(), [ self.assertEqual(self.registry.paths(), [

View File

@@ -11,6 +11,7 @@ from mozpack.files import (
XPTFile, XPTFile,
MinifiedProperties, MinifiedProperties,
FileFinder, FileFinder,
JarFinder,
) )
from mozpack.mozjar import ( from mozpack.mozjar import (
JarReader, JarReader,
@@ -486,7 +487,7 @@ class TestMinifiedProperties(TestWithTmpDir):
class MatchTestTemplate(object): class MatchTestTemplate(object):
def do_match_test(self): def prepare_match_test(self, with_dotfiles=False):
self.add('bar') self.add('bar')
self.add('foo/bar') self.add('foo/bar')
self.add('foo/baz') self.add('foo/baz')
@@ -494,7 +495,11 @@ class MatchTestTemplate(object):
self.add('foo/qux/bar') self.add('foo/qux/bar')
self.add('foo/qux/2/test') self.add('foo/qux/2/test')
self.add('foo/qux/2/test2') 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('', [ self.do_check('', [
'bar', 'foo/bar', 'foo/baz', 'foo/qux/1', 'foo/qux/bar', 'bar', 'foo/bar', 'foo/baz', 'foo/qux/1', 'foo/qux/bar',
'foo/qux/2/test', 'foo/qux/2/test2' 'foo/qux/2/test', 'foo/qux/2/test2'
@@ -533,6 +538,33 @@ class MatchTestTemplate(object):
self.do_check('**/barbaz', []) self.do_check('**/barbaz', [])
self.do_check('f**/bar', ['foo/bar']) 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): class TestFileFinder(MatchTestTemplate, TestWithTmpDir):
def add(self, path): def add(self, path):
@@ -540,34 +572,30 @@ class TestFileFinder(MatchTestTemplate, TestWithTmpDir):
open(self.tmppath(path), 'wb').write(path) open(self.tmppath(path), 'wb').write(path)
def do_check(self, pattern, result): def do_check(self, pattern, result):
if result: do_check(self, self.finder, pattern, 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))
def test_file_finder(self): def test_file_finder(self):
self.prepare_match_test(with_dotfiles=True)
self.finder = FileFinder(self.tmpdir) self.finder = FileFinder(self.tmpdir)
self.do_match_test() self.do_match_test()
self.add('foo/.foo') self.do_finder_test(self.finder)
self.add('foo/.bar/foo')
self.assertTrue(self.finder.contains('foo/.foo'))
self.assertTrue(self.finder.contains('foo/.bar')) class TestJarFinder(MatchTestTemplate, TestWithTmpDir):
self.assertTrue('foo/.foo' in [f for f, c in def add(self, path):
self.finder.find('foo/.foo')]) self.jar.add(path, path, compress=True)
self.assertTrue('foo/.bar/foo' in [f for f, c in
self.finder.find('foo/.bar')]) def do_check(self, pattern, result):
self.assertEqual(sorted([f for f, c in self.finder.find('foo/.*')]), do_check(self, self.finder, pattern, result)
['foo/.bar/foo', 'foo/.foo'])
for pattern in ['foo', '**', '**/*', '**/foo', 'foo/*']: def test_jar_finder(self):
self.assertFalse('foo/.foo' in [f for f, c in self.jar = JarWriter(file=self.tmppath('test.jar'))
self.finder.find(pattern)]) self.prepare_match_test()
self.assertFalse('foo/.bar/foo' in [f for f, c in self.jar.finish()
self.finder.find(pattern)]) reader = JarReader(file=self.tmppath('test.jar'))
self.assertEqual(sorted([f for f, c in self.finder.find(pattern)]), self.finder = JarFinder(self.tmppath('test.jar'), reader)
sorted([f for f, c in self.finder self.do_match_test()
if mozpack.path.match(f, pattern)]))
if __name__ == '__main__': if __name__ == '__main__':
mozunit.main() mozunit.main()

View File

@@ -9,8 +9,17 @@ from mozpack.unify import (
import mozunit import mozunit
from mozpack.test.test_files import TestWithTmpDir from mozpack.test.test_files import TestWithTmpDir
from mozpack.copier import ensure_parent_dir 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 import os
from mozpack.errors import ErrorMessage import sys
from mozpack.errors import (
ErrorMessage,
AccumulatedErrors,
errors,
)
class TestUnified(TestWithTmpDir): class TestUnified(TestWithTmpDir):
@@ -36,7 +45,8 @@ class TestUnifiedFinder(TestUnified):
self.create_one('b', 'test/foo', 'b\nc\na\n') self.create_one('b', 'test/foo', 'b\nc\na\n')
self.create_both('test/bar', 'a\nb\nc\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']) sorted=['test'])
self.assertEqual(sorted([(f, c.open().read()) self.assertEqual(sorted([(f, c.open().read())
for f, c in finder.find('foo')]), for f, c in finder.find('foo')]),
@@ -73,7 +83,8 @@ class TestUnifiedBuildFinder(TestUnified):
'</body>', '</body>',
'</html>', '</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 self.assertEqual(sorted([(f, c.open().read()) for f, c in
finder.find('**/chrome.manifest')]), finder.find('**/chrome.manifest')]),
[('chrome.manifest', 'a\nb\nc\n'), [('chrome.manifest', 'a\nb\nc\n'),
@@ -92,6 +103,25 @@ class TestUnifiedBuildFinder(TestUnified):
'</html>', '</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__': if __name__ == '__main__':
mozunit.main() mozunit.main()

View File

@@ -3,7 +3,8 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
from mozpack.files import ( from mozpack.files import (
FileFinder, BaseFinder,
JarFinder,
ExecutableFile, ExecutableFile,
BaseFile, BaseFile,
GeneratedFile, GeneratedFile,
@@ -13,6 +14,7 @@ from mozpack.executables import (
may_strip, may_strip,
strip, strip,
) )
from mozpack.mozjar import JarReader
from mozpack.errors import errors from mozpack.errors import errors
from tempfile import mkstemp from tempfile import mkstemp
import mozpack.path import mozpack.path
@@ -67,66 +69,70 @@ class UnifiedExecutableFile(BaseFile):
os.unlink(f) os.unlink(f)
class UnifiedFinder(FileFinder): class UnifiedFinder(BaseFinder):
''' '''
Helper to get unified BaseFile instances from two distinct trees on the Helper to get unified BaseFile instances from two distinct trees on the
file system. 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 Initialize a UnifiedFinder. finder1 and finder2 are BaseFinder
for the two trees from which files are picked. UnifiedFinder.find() instances from which files are picked. UnifiedFinder.find() will act as
will act as FileFinder.find() but will error out when matches can only FileFinder.find() but will error out when matches can only be found in
be found in one of the two trees and not the other. It will also error one of the two trees and not the other. It will also error out if
out if matches can be found on both ends but their contents are not matches can be found on both ends but their contents are not identical.
identical.
The sorted argument gives a list of mozpack.path.match patterns. File The sorted argument gives a list of mozpack.path.match patterns. File
paths matching one of these patterns will have their contents compared paths matching one of these patterns will have their contents compared
with their lines sorted. with their lines sorted.
''' '''
self._base1 = FileFinder(base1, **kargs) assert isinstance(finder1, BaseFinder)
self._base2 = FileFinder(base2, **kargs) assert isinstance(finder2, BaseFinder)
self._finder1 = finder1
self._finder2 = finder2
self._sorted = sorted self._sorted = sorted
BaseFinder.__init__(self, finder1.base, **kargs)
def _find(self, path): def _find(self, path):
''' '''
UnifiedFinder.find() implementation. UnifiedFinder.find() implementation.
''' '''
files1 = OrderedDict() files1 = OrderedDict()
for p, f in self._base1.find(path): for p, f in self._finder1.find(path):
files1[p] = f files1[p] = f
files2 = set() files2 = set()
for p, f in self._base2.find(path): for p, f in self._finder2.find(path):
files2.add(p) files2.add(p)
if p in files1: if p in files1:
if may_unify_binary(files1[p]) and \ if may_unify_binary(files1[p]) and \
may_unify_binary(f): may_unify_binary(f):
yield p, UnifiedExecutableFile(files1[p].path, f.path) yield p, UnifiedExecutableFile(files1[p].path, f.path)
else: else:
err = errors.count
unified = self.unify_file(p, files1[p], f) unified = self.unify_file(p, files1[p], f)
if unified: if unified:
yield p, unified yield p, unified
else: elif err == errors.count:
self._report_difference(p, files1[p], f) self._report_difference(p, files1[p], f)
else: 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]: 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): def _report_difference(self, path, file1, file2):
''' '''
Report differences between files in both trees. Report differences between files in both trees.
''' '''
errors.error("Can't unify %s: file differs between %s and %s" % 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 \ if not isinstance(file1, ExecutableFile) and \
not isinstance(file2, ExecutableFile): not isinstance(file2, ExecutableFile):
from difflib import unified_diff from difflib import unified_diff
for line in unified_diff(file1.open().readlines(), for line in unified_diff(file1.open().readlines(),
file2.open().readlines(), file2.open().readlines(),
os.path.join(self._base1.base, path), os.path.join(self._finder1.base, path),
os.path.join(self._base2.base, path)): os.path.join(self._finder2.base, path)):
errors.out.write(line) errors.out.write(line)
def unify_file(self, path, file1, file2): def unify_file(self, path, file1, file2):
@@ -152,8 +158,8 @@ class UnifiedBuildFinder(UnifiedFinder):
"*.manifest" files to differ in their order, and unifies "buildconfig.html" "*.manifest" files to differ in their order, and unifies "buildconfig.html"
files by merging their content. files by merging their content.
''' '''
def __init__(self, base1, base2, **kargs): def __init__(self, finder1, finder2, **kargs):
UnifiedFinder.__init__(self, base1, base2, UnifiedFinder.__init__(self, finder1, finder2,
sorted=['**/*.manifest'], **kargs) sorted=['**/*.manifest'], **kargs)
def unify_file(self, path, file1, file2): def unify_file(self, path, file1, file2):
@@ -171,4 +177,15 @@ class UnifiedBuildFinder(UnifiedFinder):
['<hr> </hr>\n'] + ['<hr> </hr>\n'] +
content2[content2.index('<h1>about:buildconfig</h1>\n') + 1:] 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) return UnifiedFinder.unify_file(self, path, file1, file2)

View File

@@ -294,7 +294,8 @@ def main():
with errors.accumulate(): with errors.accumulate():
if args.unify: if args.unify:
finder = UnifiedBuildFinder(args.source, args.unify, finder = UnifiedBuildFinder(FileFinder(args.source),
FileFinder(args.unify),
minify=args.minify) minify=args.minify)
else: else:
finder = FileFinder(args.source, minify=args.minify) finder = FileFinder(args.source, minify=args.minify)