unify.py (9984B)
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 import os 6 import re 7 import struct 8 import subprocess 9 from collections import OrderedDict 10 from tempfile import mkstemp 11 12 import buildconfig 13 14 import mozpack.path as mozpath 15 from mozbuild.util import hexdump 16 from mozpack.errors import errors 17 from mozpack.executables import MACHO_SIGNATURES 18 from mozpack.files import BaseFile, BaseFinder, ExecutableFile, GeneratedFile 19 20 # Regular expressions for unifying install.rdf 21 FIND_TARGET_PLATFORM = re.compile( 22 r""" 23 <(?P<ns>[-._0-9A-Za-z]+:)?targetPlatform> # The targetPlatform tag, with any namespace 24 (?P<platform>[^<]*) # The actual platform value 25 </(?P=ns)?targetPlatform> # The closing tag 26 """, 27 re.X, 28 ) 29 FIND_TARGET_PLATFORM_ATTR = re.compile( 30 r""" 31 (?P<tag><(?:[-._0-9A-Za-z]+:)?Description) # The opening part of the <Description> tag 32 (?P<attrs>[^>]*?)\s+ # The initial attributes 33 (?P<ns>[-._0-9A-Za-z]+:)?targetPlatform= # The targetPlatform attribute, with any namespace 34 [\'"](?P<platform>[^\'"]+)[\'"] # The actual platform value 35 (?P<otherattrs>[^>]*?>) # The remaining attributes and closing angle bracket 36 """, 37 re.X, 38 ) 39 40 41 def may_unify_binary(file): 42 """ 43 Return whether the given BaseFile instance is an ExecutableFile that 44 may be unified. Only non-fat Mach-O binaries are to be unified. 45 """ 46 if isinstance(file, ExecutableFile): 47 signature = file.open().read(4) 48 if len(signature) < 4: 49 return False 50 signature = struct.unpack(">L", signature)[0] 51 if signature in MACHO_SIGNATURES: 52 return True 53 return False 54 55 56 class UnifiedExecutableFile(BaseFile): 57 """ 58 File class for executable and library files that to be unified with 'lipo'. 59 """ 60 61 def __init__(self, executable1, executable2): 62 """ 63 Initialize a UnifiedExecutableFile with a pair of ExecutableFiles to 64 be unified. They are expected to be non-fat Mach-O executables. 65 """ 66 assert isinstance(executable1, ExecutableFile) 67 assert isinstance(executable2, ExecutableFile) 68 self._executables = (executable1, executable2) 69 70 def copy(self, dest, skip_if_older=True): 71 """ 72 Create a fat executable from the two Mach-O executable given when 73 creating the instance. 74 skip_if_older is ignored. 75 """ 76 assert isinstance(dest, str) 77 tmpfiles = [] 78 try: 79 for e in self._executables: 80 fd, f = mkstemp() 81 os.close(fd) 82 tmpfiles.append(f) 83 e.copy(f, skip_if_older=False) 84 lipo = buildconfig.substs.get("LIPO") or "lipo" 85 subprocess.check_call([lipo, "-create"] + tmpfiles + ["-output", dest]) 86 except Exception as e: 87 errors.error( 88 "Failed to unify %s and %s: %s" 89 % (self._executables[0].path, self._executables[1].path, str(e)) 90 ) 91 finally: 92 for f in tmpfiles: 93 os.unlink(f) 94 95 96 class UnifiedFinder(BaseFinder): 97 """ 98 Helper to get unified BaseFile instances from two distinct trees on the 99 file system. 100 """ 101 102 def __init__(self, finder1, finder2, sorted=[], **kargs): 103 """ 104 Initialize a UnifiedFinder. finder1 and finder2 are BaseFinder 105 instances from which files are picked. UnifiedFinder.find() will act as 106 FileFinder.find() but will error out when matches can only be found in 107 one of the two trees and not the other. It will also error out if 108 matches can be found on both ends but their contents are not identical. 109 110 The sorted argument gives a list of mozpath.match patterns. File 111 paths matching one of these patterns will have their contents compared 112 with their lines sorted. 113 """ 114 assert isinstance(finder1, BaseFinder) 115 assert isinstance(finder2, BaseFinder) 116 self._finder1 = finder1 117 self._finder2 = finder2 118 self._sorted = sorted 119 BaseFinder.__init__(self, finder1.base, **kargs) 120 121 def _find(self, path): 122 """ 123 UnifiedFinder.find() implementation. 124 """ 125 # There is no `OrderedSet`. Operator `|` was added only in 126 # Python 3.9, so we merge by hand. 127 all_paths = OrderedDict() 128 129 files1 = OrderedDict() 130 for p, f in self._finder1.find(path): 131 files1[p] = f 132 all_paths[p] = True 133 files2 = OrderedDict() 134 for p, f in self._finder2.find(path): 135 files2[p] = f 136 all_paths[p] = True 137 138 for p in all_paths: 139 err = errors.count 140 unified = self.unify_file(p, files1.get(p), files2.get(p)) 141 if unified: 142 yield p, unified 143 elif err == errors.count: # No errors have already been reported. 144 self._report_difference(p, files1.get(p), files2.get(p)) 145 146 def _report_difference(self, path, file1, file2): 147 """ 148 Report differences between files in both trees. 149 """ 150 if not file1: 151 errors.error("File missing in %s: %s" % (self._finder1.base, path)) 152 return 153 if not file2: 154 errors.error("File missing in %s: %s" % (self._finder2.base, path)) 155 return 156 157 errors.error( 158 "Can't unify %s: file differs between %s and %s" 159 % (path, self._finder1.base, self._finder2.base) 160 ) 161 if not isinstance(file1, ExecutableFile) and not isinstance( 162 file2, ExecutableFile 163 ): 164 from difflib import unified_diff 165 166 try: 167 lines1 = [l.decode("utf-8") for l in file1.open().readlines()] 168 lines2 = [l.decode("utf-8") for l in file2.open().readlines()] 169 except UnicodeDecodeError: 170 lines1 = hexdump(file1.open().read()) 171 lines2 = hexdump(file2.open().read()) 172 173 for line in unified_diff( 174 lines1, 175 lines2, 176 os.path.join(self._finder1.base, path), 177 os.path.join(self._finder2.base, path), 178 ): 179 errors.out.write(line) 180 181 def unify_file(self, path, file1, file2): 182 """ 183 Given two BaseFiles and the path they were found at, return a 184 unified version of the files. If the files match, the first BaseFile 185 may be returned. 186 If the files don't match or one of them is `None`, the method returns 187 `None`. 188 Subclasses may decide to unify by using one of the files in that case. 189 """ 190 if not file1 or not file2: 191 return None 192 193 if may_unify_binary(file1) and may_unify_binary(file2): 194 return UnifiedExecutableFile(file1, file2) 195 196 content1 = file1.open().readlines() 197 content2 = file2.open().readlines() 198 if content1 == content2: 199 return file1 200 for pattern in self._sorted: 201 if mozpath.match(path, pattern): 202 if sorted(content1) == sorted(content2): 203 return file1 204 break 205 return None 206 207 208 class UnifiedBuildFinder(UnifiedFinder): 209 """ 210 Specialized UnifiedFinder for Mozilla applications packaging. It allows 211 ``*.manifest`` files to differ in their order, and unifies ``buildconfig.html`` 212 files by merging their content. 213 """ 214 215 def __init__(self, finder1, finder2, **kargs): 216 UnifiedFinder.__init__( 217 self, finder1, finder2, sorted=["**/*.manifest"], **kargs 218 ) 219 220 def unify_file(self, path, file1, file2): 221 """ 222 Unify files taking Mozilla application special cases into account. 223 Otherwise defer to UnifiedFinder.unify_file. 224 """ 225 basename = mozpath.basename(path) 226 if file1 and file2 and basename == "buildconfig.html": 227 content1 = file1.open().readlines() 228 content2 = file2.open().readlines() 229 # Copy everything from the first file up to the end of its <div>, 230 # insert a <hr> between the two files and copy the second file's 231 # content beginning after its leading <h1>. 232 return GeneratedFile( 233 b"".join( 234 content1[: content1.index(b" </div>\n")] 235 + [b" <hr> </hr>\n"] 236 + content2[ 237 content2.index(b" <h1>Build Configuration</h1>\n") + 1 : 238 ] 239 ) 240 ) 241 elif file1 and file2 and basename == "install.rdf": 242 # install.rdf files often have em:targetPlatform (either as 243 # attribute or as tag) that will differ between platforms. The 244 # unified install.rdf should contain both em:targetPlatforms if 245 # they exist, or strip them if only one file has a target platform. 246 content1, content2 = ( 247 FIND_TARGET_PLATFORM_ATTR.sub( 248 lambda m: m.group("tag") 249 + m.group("attrs") 250 + m.group("otherattrs") 251 + "<%stargetPlatform>%s</%stargetPlatform>" 252 % (m.group("ns") or "", m.group("platform"), m.group("ns") or ""), 253 f.open().read().decode("utf-8"), 254 ) 255 for f in (file1, file2) 256 ) 257 258 platform2 = FIND_TARGET_PLATFORM.search(content2) 259 return GeneratedFile( 260 FIND_TARGET_PLATFORM.sub( 261 lambda m: m.group(0) + platform2.group(0) if platform2 else "", 262 content1, 263 ) 264 ) 265 return UnifiedFinder.unify_file(self, path, file1, file2)