|
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/. |
|
4 |
|
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 |
|
23 |
|
24 |
|
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 |
|
38 |
|
39 |
|
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) |
|
52 |
|
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) |
|
71 |
|
72 |
|
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. |
|
85 |
|
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) |
|
96 |
|
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)) |
|
123 |
|
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) |
|
138 |
|
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 |
|
154 |
|
155 |
|
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) |
|
165 |
|
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) |