fat_aar.py (7676B)
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 """ 6 Fetch and unpack architecture-specific Maven zips, verify cross-architecture 7 compatibility, and ready inputs to an Android multi-architecture fat AAR build. 8 """ 9 10 import argparse 11 import subprocess 12 import sys 13 from collections import OrderedDict, defaultdict 14 from hashlib import sha1 # We don't need a strong hash to compare inputs. 15 from io import BytesIO 16 from zipfile import ZipFile 17 18 import buildconfig 19 import mozpack.path as mozpath 20 from mozpack.copier import FileCopier 21 from mozpack.files import JarFinder 22 from mozpack.mozjar import JarReader 23 from mozpack.packager.unpack import UnpackFinder 24 25 26 def _download_zip(distdir, arch): 27 # The mapping from Android CPU architecture to TC job is defined here, and the TC index 28 # lookup is mediated by python/mozbuild/mozbuild/artifacts.py and 29 # python/mozbuild/mozbuild/artifact_builds.py. 30 jobs = { 31 "arm64-v8a": "android-aarch64-opt", 32 "armeabi-v7a": "android-arm-opt", 33 "x86_64": "android-x86_64-opt", 34 } 35 36 dest = mozpath.join(distdir, "input", arch) 37 subprocess.check_call([ 38 sys.executable, 39 mozpath.join(buildconfig.topsrcdir, "mach"), 40 "artifact", 41 "install", 42 "--job", 43 jobs[arch], 44 "--distdir", 45 dest, 46 "--no-tests", 47 "--no-process", 48 "--maven-zip", 49 ]) 50 return mozpath.join(dest, "target.maven.zip") 51 52 53 def fat_aar(distdir, zip_paths, no_process=False, no_compatibility_check=False): 54 if no_process: 55 print("Not processing architecture-specific artifact Maven AARs.") 56 return 0 57 58 # Map {filename: {fingerprint: [arch1, arch2, ...]}}. 59 diffs = defaultdict(lambda: defaultdict(list)) 60 missing_arch_prefs = set() 61 # Collect multi-architecture inputs to the fat AAR. 62 copier = FileCopier() 63 64 for arch, zip_path in zip_paths.items(): 65 if not zip_path: 66 zip_path = _download_zip(distdir, arch) 67 # Map old non-architecture-specific path to new architecture-specific path. 68 old_rewrite_map = { 69 "greprefs.js": f"{arch}/greprefs.js", 70 "defaults/pref/geckoview-prefs.js": f"defaults/pref/{arch}/geckoview-prefs.js", 71 } 72 73 # Architecture-specific preferences files. 74 arch_prefs = set(old_rewrite_map.values()) 75 missing_arch_prefs |= set(arch_prefs) 76 77 aars = [ 78 (path, file) 79 for (path, file) in JarFinder(zip_path, JarReader(zip_path)).find( 80 "**/geckoview-*.aar" 81 ) 82 if "exoplayer2" not in path 83 ] 84 if len(aars) != 1: 85 raise ValueError( 86 f'Maven zip "{zip_path}" with more than one candidate AAR found: {sorted(p for p, _ in aars)}' 87 ) 88 aar_path, aar_file = aars[0] 89 90 jar_finder = JarFinder( 91 aar_file.file.filename, JarReader(fileobj=aar_file.open()) 92 ) 93 for path, fileobj in UnpackFinder(jar_finder): 94 # Native libraries go straight through. 95 if mozpath.match(path, "jni/**"): 96 copier.add(path, fileobj) 97 98 elif path in arch_prefs: 99 copier.add(path, fileobj) 100 101 elif path in ("classes.jar", "annotations.zip"): 102 # annotations.zip differs due to timestamps, but the contents should not. 103 104 # `JarReader` fails on the non-standard `classes.jar` produced by Gradle/aapt, 105 # and it's not worth working around, so we use Python's zip functionality 106 # instead. 107 z = ZipFile(BytesIO(fileobj.open().read())) 108 for r in z.namelist(): 109 fingerprint = sha1(z.open(r).read()).hexdigest() 110 diffs[f"{path}!/{r}"][fingerprint].append(arch) 111 112 else: 113 fingerprint = sha1(fileobj.open().read()).hexdigest() 114 # There's no need to distinguish `target.maven.zip` from `assets/omni.ja` here, 115 # since in practice they will never overlap. 116 diffs[path][fingerprint].append(arch) 117 118 missing_arch_prefs.discard(path) 119 120 # Some differences are allowed across the architecture-specific AARs. We could allow-list 121 # the actual content, but it's not necessary right now. 122 allow_pattern_list = { 123 "AndroidManifest.xml", # Min SDK version is different for 32- and 64-bit builds. 124 "classes.jar!/org/mozilla/gecko/util/HardwareUtils.class", # Min SDK as well. 125 "classes.jar!/org/mozilla/geckoview/BuildConfig.class", 126 # Each input captures its CPU architecture. 127 "chrome/toolkit/content/global/buildconfig.html", 128 # Bug 1556162: localized resources are not deterministic across 129 # per-architecture builds triggered from the same push. 130 "**/*.ftl", 131 "**/*.dtd", 132 "**/*.properties", 133 } 134 135 not_allowed = OrderedDict() 136 137 def format_diffs(ds): 138 # Like ' armeabi-v7a, arm64-v8a -> XXX\n x86_64 -> YYY'. 139 return "\n".join( 140 sorted( 141 " {archs} -> {fingerprint}".format( 142 archs=", ".join(sorted(archs)), fingerprint=fingerprint 143 ) 144 for fingerprint, archs in ds.items() 145 ) 146 ) 147 148 for p, ds in sorted(diffs.items()): 149 if len(ds) <= 1: 150 # Only one hash across all inputs: roll on. 151 continue 152 153 if any(mozpath.match(p, pat) for pat in allow_pattern_list): 154 print( 155 f'Allowed: Path "{p}" has architecture-specific versions:\n{format_diffs(ds)}' 156 ) 157 continue 158 159 not_allowed[p] = ds 160 161 for p, ds in not_allowed.items(): 162 print( 163 f'Disallowed: Path "{p}" has architecture-specific versions:\n{format_diffs(ds)}' 164 ) 165 166 for missing in sorted(missing_arch_prefs): 167 print( 168 f"Disallowed: Inputs missing expected architecture-specific input: {missing}" 169 ) 170 171 if not no_compatibility_check and (missing_arch_prefs or not_allowed): 172 return 1 173 174 output_dir = mozpath.join(distdir, "output") 175 copier.copy(output_dir) 176 177 return 0 178 179 180 _ALL_ARCHS = ("armeabi-v7a", "arm64-v8a", "x86_64") 181 182 183 def main(argv): 184 description = """Fetch and unpack architecture-specific Maven zips, verify cross-architecture 185 compatibility, and ready inputs to an Android multi-architecture fat AAR build.""" 186 187 parser = argparse.ArgumentParser(description=description) 188 parser.add_argument("architectures", metavar="arch", nargs="+", choices=_ALL_ARCHS) 189 parser.add_argument( 190 "--no-process", action="store_true", help="Do not process Maven AARs." 191 ) 192 parser.add_argument( 193 "--no-compatibility-check", 194 action="store_true", 195 help="Do not fail if Maven AARs are not compatible.", 196 ) 197 parser.add_argument("--distdir", required=True) 198 199 for arch in _ALL_ARCHS: 200 command_line_flag = arch.replace("_", "-") 201 parser.add_argument(f"--{command_line_flag}", dest=arch) 202 203 args = parser.parse_args(argv) 204 args_dict = vars(args) 205 206 zip_paths = {arch: args_dict.get(arch) for arch in args.architectures} 207 208 if not zip_paths: 209 raise ValueError("You must provide at least one Maven zip!") 210 211 return fat_aar( 212 args.distdir, 213 zip_paths, 214 no_process=args.no_process, 215 no_compatibility_check=args.no_compatibility_check, 216 ) 217 218 219 if __name__ == "__main__": 220 sys.exit(main(sys.argv[1:]))