tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:]))