python/mozbuild/mozpack/unify.py

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

     1 # This Source Code Form is subject to the terms of the Mozilla Public
     2 # License, v. 2.0. If a copy of the MPL was not distributed with this
     3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
     5 from mozpack.files import (
     6     BaseFinder,
     7     JarFinder,
     8     ExecutableFile,
     9     BaseFile,
    10     GeneratedFile,
    11 )
    12 from mozpack.executables import (
    13     MACHO_SIGNATURES,
    14 )
    15 from mozpack.mozjar import JarReader
    16 from mozpack.errors import errors
    17 from tempfile import mkstemp
    18 import mozpack.path
    19 import struct
    20 import os
    21 import subprocess
    22 from collections import OrderedDict
    25 def may_unify_binary(file):
    26     '''
    27     Return whether the given BaseFile instance is an ExecutableFile that
    28     may be unified. Only non-fat Mach-O binaries are to be unified.
    29     '''
    30     if isinstance(file, ExecutableFile):
    31         signature = file.open().read(4)
    32         if len(signature) < 4:
    33             return False
    34         signature = struct.unpack('>L', signature)[0]
    35         if signature in MACHO_SIGNATURES:
    36             return True
    37     return False
    40 class UnifiedExecutableFile(BaseFile):
    41     '''
    42     File class for executable and library files that to be unified with 'lipo'.
    43     '''
    44     def __init__(self, executable1, executable2):
    45         '''
    46         Initialize a UnifiedExecutableFile with a pair of ExecutableFiles to
    47         be unified. They are expected to be non-fat Mach-O executables.
    48         '''
    49         assert isinstance(executable1, ExecutableFile)
    50         assert isinstance(executable2, ExecutableFile)
    51         self._executables = (executable1, executable2)
    53     def copy(self, dest, skip_if_older=True):
    54         '''
    55         Create a fat executable from the two Mach-O executable given when
    56         creating the instance.
    57         skip_if_older is ignored.
    58         '''
    59         assert isinstance(dest, basestring)
    60         tmpfiles = []
    61         try:
    62             for e in self._executables:
    63                 fd, f = mkstemp()
    64                 os.close(fd)
    65                 tmpfiles.append(f)
    66                 e.copy(f, skip_if_older=False)
    67             subprocess.call(['lipo', '-create'] + tmpfiles + ['-output', dest])
    68         finally:
    69             for f in tmpfiles:
    70                 os.unlink(f)
    73 class UnifiedFinder(BaseFinder):
    74     '''
    75     Helper to get unified BaseFile instances from two distinct trees on the
    76     file system.
    77     '''
    78     def __init__(self, finder1, finder2, sorted=[], **kargs):
    79         '''
    80         Initialize a UnifiedFinder. finder1 and finder2 are BaseFinder
    81         instances from which files are picked. UnifiedFinder.find() will act as
    82         FileFinder.find() but will error out when matches can only be found in
    83         one of the two trees and not the other. It will also error out if
    84         matches can be found on both ends but their contents are not identical.
    86         The sorted argument gives a list of mozpack.path.match patterns. File
    87         paths matching one of these patterns will have their contents compared
    88         with their lines sorted.
    89         '''
    90         assert isinstance(finder1, BaseFinder)
    91         assert isinstance(finder2, BaseFinder)
    92         self._finder1 = finder1
    93         self._finder2 = finder2
    94         self._sorted = sorted
    95         BaseFinder.__init__(self, finder1.base, **kargs)
    97     def _find(self, path):
    98         '''
    99         UnifiedFinder.find() implementation.
   100         '''
   101         files1 = OrderedDict()
   102         for p, f in self._finder1.find(path):
   103             files1[p] = f
   104         files2 = set()
   105         for p, f in self._finder2.find(path):
   106             files2.add(p)
   107             if p in files1:
   108                 if may_unify_binary(files1[p]) and \
   109                         may_unify_binary(f):
   110                     yield p, UnifiedExecutableFile(files1[p], f)
   111                 else:
   112                     err = errors.count
   113                     unified = self.unify_file(p, files1[p], f)
   114                     if unified:
   115                         yield p, unified
   116                     elif err == errors.count:
   117                         self._report_difference(p, files1[p], f)
   118             else:
   119                 errors.error('File missing in %s: %s' %
   120                              (self._finder1.base, p))
   121         for p in [p for p in files1 if not p in files2]:
   122             errors.error('File missing in %s: %s' % (self._finder2.base, p))
   124     def _report_difference(self, path, file1, file2):
   125         '''
   126         Report differences between files in both trees.
   127         '''
   128         errors.error("Can't unify %s: file differs between %s and %s" %
   129                      (path, self._finder1.base, self._finder2.base))
   130         if not isinstance(file1, ExecutableFile) and \
   131                 not isinstance(file2, ExecutableFile):
   132             from difflib import unified_diff
   133             for line in unified_diff(file1.open().readlines(),
   134                                      file2.open().readlines(),
   135                                      os.path.join(self._finder1.base, path),
   136                                      os.path.join(self._finder2.base, path)):
   137                 errors.out.write(line)
   139     def unify_file(self, path, file1, file2):
   140         '''
   141         Given two BaseFiles and the path they were found at, check whether
   142         their content match and return the first BaseFile if they do.
   143         '''
   144         content1 = file1.open().readlines()
   145         content2 = file2.open().readlines()
   146         if content1 == content2:
   147             return file1
   148         for pattern in self._sorted:
   149             if mozpack.path.match(path, pattern):
   150                 if sorted(content1) == sorted(content2):
   151                     return file1
   152                 break
   153         return None
   156 class UnifiedBuildFinder(UnifiedFinder):
   157     '''
   158     Specialized UnifiedFinder for Mozilla applications packaging. It allows
   159     "*.manifest" files to differ in their order, and unifies "buildconfig.html"
   160     files by merging their content.
   161     '''
   162     def __init__(self, finder1, finder2, **kargs):
   163         UnifiedFinder.__init__(self, finder1, finder2,
   164                                sorted=['**/*.manifest'], **kargs)
   166     def unify_file(self, path, file1, file2):
   167         '''
   168         Unify buildconfig.html contents, or defer to UnifiedFinder.unify_file.
   169         '''
   170         if mozpack.path.basename(path) == 'buildconfig.html':
   171             content1 = file1.open().readlines()
   172             content2 = file2.open().readlines()
   173             # Copy everything from the first file up to the end of its <body>,
   174             # insert a <hr> between the two files and copy the second file's
   175             # content beginning after its leading <h1>.
   176             return GeneratedFile(''.join(
   177                 content1[:content1.index('</body>\n')] +
   178                 ['<hr> </hr>\n'] +
   179                 content2[content2.index('<h1>about:buildconfig</h1>\n') + 1:]
   180             ))
   181         if path.endswith('.xpi'):
   182             finder1 = JarFinder(os.path.join(self._finder1.base, path),
   183                                 JarReader(fileobj=file1.open()))
   184             finder2 = JarFinder(os.path.join(self._finder2.base, path),
   185                                 JarReader(fileobj=file2.open()))
   186             unifier = UnifiedFinder(finder1, finder2, sorted=self._sorted)
   187             err = errors.count
   188             all(unifier.find(''))
   189             if err == errors.count:
   190                 return file1
   191             return None
   192         return UnifiedFinder.unify_file(self, path, file1, file2)

mercurial