diff -r 000000000000 -r 6474c204b198 python/mozbuild/mozpack/unify.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/python/mozbuild/mozpack/unify.py Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,192 @@ +# 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 ( + BaseFinder, + JarFinder, + ExecutableFile, + BaseFile, + GeneratedFile, +) +from mozpack.executables import ( + MACHO_SIGNATURES, +) +from mozpack.mozjar import JarReader +from mozpack.errors import errors +from tempfile import mkstemp +import mozpack.path +import struct +import os +import subprocess +from collections import OrderedDict + + +def may_unify_binary(file): + ''' + Return whether the given BaseFile instance is an ExecutableFile that + may be unified. Only non-fat Mach-O binaries are to be unified. + ''' + if isinstance(file, ExecutableFile): + signature = file.open().read(4) + if len(signature) < 4: + return False + signature = struct.unpack('>L', signature)[0] + if signature in MACHO_SIGNATURES: + return True + return False + + +class UnifiedExecutableFile(BaseFile): + ''' + File class for executable and library files that to be unified with 'lipo'. + ''' + def __init__(self, executable1, executable2): + ''' + Initialize a UnifiedExecutableFile with a pair of ExecutableFiles to + be unified. They are expected to be non-fat Mach-O executables. + ''' + assert isinstance(executable1, ExecutableFile) + assert isinstance(executable2, ExecutableFile) + self._executables = (executable1, executable2) + + def copy(self, dest, skip_if_older=True): + ''' + Create a fat executable from the two Mach-O executable given when + creating the instance. + skip_if_older is ignored. + ''' + assert isinstance(dest, basestring) + tmpfiles = [] + try: + for e in self._executables: + fd, f = mkstemp() + os.close(fd) + tmpfiles.append(f) + e.copy(f, skip_if_older=False) + subprocess.call(['lipo', '-create'] + tmpfiles + ['-output', dest]) + finally: + for f in tmpfiles: + os.unlink(f) + + +class UnifiedFinder(BaseFinder): + ''' + Helper to get unified BaseFile instances from two distinct trees on the + file system. + ''' + def __init__(self, finder1, finder2, sorted=[], **kargs): + ''' + 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. + ''' + 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._finder1.find(path): + files1[p] = f + files2 = set() + 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], f) + else: + err = errors.count + unified = self.unify_file(p, files1[p], f) + if unified: + yield p, unified + elif err == errors.count: + self._report_difference(p, files1[p], f) + else: + 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._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._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._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() + content2 = file2.open().readlines() + if content1 == content2: + return file1 + for pattern in self._sorted: + if mozpack.path.match(path, pattern): + if sorted(content1) == sorted(content2): + return file1 + break + return None + + +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, 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() + content2 = file2.open().readlines() + # Copy everything from the first file up to the end of its , + # insert a
between the two files and copy the second file's + # content beginning after its leading

. + return GeneratedFile(''.join( + content1[:content1.index('\n')] + + ['
\n'] + + content2[content2.index('

about:buildconfig

\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)