Fri, 16 Jan 2015 18:13:44 +0100
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)