| #!/usr/bin/env python3 |
| # Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file |
| # for details. All rights reserved. Use of this source code is governed by a |
| # BSD-style license that can be found in the LICENSE file. |
| |
| import glob |
| import optparse |
| import os |
| import shutil |
| import subprocess |
| import sys |
| |
| import apk_utils |
| import utils |
| import zip_utils |
| |
| USAGE = 'usage: %prog [options] <apk>' |
| |
| |
| def parse_options(): |
| parser = optparse.OptionParser(usage=USAGE) |
| parser.add_option('--clear-profile', |
| help='To remove baseline.prof and baseline.profm from ' |
| 'assets/dexopt/', |
| default=False, |
| action='store_true') |
| parser.add_option('--compress-dex', |
| help='Whether the dex should be stored compressed', |
| action='store_true', |
| default=False) |
| parser.add_option('--dex', |
| help='Directory or archive with dex files to use instead ' |
| 'of those in the apk', |
| default=None) |
| parser.add_option( |
| '--desugared-library-dex', |
| help='Path to desugared library dex file to use or archive ' |
| 'containing a single classes.dex file', |
| default=None) |
| parser.add_option( |
| '--resources', |
| help=('pattern that matches resources to use instead of ' + |
| 'those in the apk'), |
| default=None) |
| parser.add_option('--out', |
| help='output file (default ./$(basename <apk>))', |
| default=None) |
| parser.add_option('--keystore', |
| help='keystore file (default ~/.android/app.keystore)', |
| default=None) |
| parser.add_option( |
| '--install', |
| help='install the generated apk with adb options -t -r -d', |
| default=False, |
| action='store_true') |
| parser.add_option('--adb-options', |
| help='additional adb options when running adb', |
| default=None) |
| parser.add_option('--quiet', help='disable verbose logging', default=False) |
| parser.add_option('--sign-before-align', |
| help='Sign the apk before aligning', |
| default=False, |
| action='store_true') |
| (options, apks) = parser.parse_args() |
| if len(apks) == 0: |
| parser.error('Expected one or more apk arguments, got none.') |
| if len(apks) > 1 and options.out: |
| parser.error('Cannot process multiple apks with --out') |
| return (options, apks) |
| |
| |
| def is_archive(file): |
| return file.endswith('.zip') or file.endswith('.jar') |
| |
| |
| def repack(apk, clear_profile, processed_out, desugared_library_dex, |
| compress_dex, resources, temp, quiet, logging): |
| processed_apk = os.path.join(temp, 'processed.apk') |
| shutil.copyfile(apk, processed_apk) |
| |
| if clear_profile: |
| zip_utils.remove_files_from_zip( |
| ['assets/dexopt/baseline.prof', 'assets/dexopt/baseline.profm'], |
| processed_apk) |
| |
| if not processed_out: |
| if has_wrong_compression(apk, compress_dex): |
| processed_out = os.path.join(temp, 'extracted') |
| subprocess.check_call( |
| ['unzip', apk, '-d', processed_out, 'classes*.dex']) |
| else: |
| utils.Print('Using original dex as is', quiet=quiet) |
| return processed_apk |
| |
| utils.Print('Repacking APK with dex files from {}'.format(processed_out), |
| quiet=quiet) |
| |
| # Delete original dex files in APK. |
| with utils.ChangedWorkingDirectory(temp, quiet=quiet): |
| cmd = ['zip', '-d', 'processed.apk', '*.dex'] |
| utils.RunCmd(cmd, quiet=quiet, logging=logging) |
| |
| # Unzip the jar or zip file into `temp`. |
| if is_archive(processed_out): |
| cmd = ['unzip', processed_out, '-d', temp] |
| if quiet: |
| cmd.insert(1, '-q') |
| utils.RunCmd(cmd, quiet=quiet, logging=logging) |
| processed_out = temp |
| elif desugared_library_dex: |
| for dex_name in glob.glob('*.dex', root_dir=processed_out): |
| src = os.path.join(processed_out, dex_name) |
| dst = os.path.join(temp, dex_name) |
| shutil.copyfile(src, dst) |
| processed_out = temp |
| |
| if desugared_library_dex: |
| desugared_library_dex_index = len(glob.glob('*.dex', root_dir=temp)) + 1 |
| desugared_library_dex_name = 'classes%s.dex' % desugared_library_dex_index |
| desugared_library_dex_dst = os.path.join(temp, |
| desugared_library_dex_name) |
| if is_archive(desugared_library_dex): |
| zip_utils.extract_member(desugared_library_dex, 'classes.dex', |
| desugared_library_dex_dst) |
| else: |
| shutil.copyfile(desugared_library_dex, desugared_library_dex_dst) |
| |
| # Insert the new dex and resource files from `processed_out` into the APK. |
| with utils.ChangedWorkingDirectory(processed_out, quiet=quiet): |
| dex_files = glob.glob('*.dex') |
| dex_files.sort() |
| resource_files = glob.glob(resources) if resources else [] |
| cmd = ['zip', '-u'] |
| if not compress_dex: |
| cmd.append('-0') |
| cmd.append(processed_apk) |
| cmd.extend(dex_files) |
| cmd.extend(resource_files) |
| utils.RunCmd(cmd, quiet=quiet, logging=logging) |
| return processed_apk |
| |
| |
| def sign(unsigned_apk, keystore, temp, quiet, logging): |
| signed_apk = os.path.join(temp, 'unaligned.apk') |
| return apk_utils.sign_with_apksigner(unsigned_apk, |
| signed_apk, |
| keystore, |
| quiet=quiet, |
| logging=logging) |
| |
| |
| def align(signed_apk, temp, quiet, logging): |
| utils.Print('Aligning', quiet=quiet) |
| aligned_apk = os.path.join(temp, 'aligned.apk') |
| return apk_utils.align(signed_apk, aligned_apk) |
| |
| |
| def has_wrong_compression(apk, compress_dex): |
| cmd = ['zipinfo', apk, 'classes*.dex'] |
| stdout = subprocess.check_output(cmd).decode('utf-8').strip() |
| expected = ' defN ' if compress_dex else ' stor ' |
| for line in stdout.splitlines(): |
| if not expected in line: |
| return True |
| return False |
| |
| |
| def masseur(apk, |
| clear_profile=False, |
| dex=None, |
| desugared_library_dex=None, |
| compress_dex=False, |
| resources=None, |
| out=None, |
| adb_options=None, |
| sign_before_align=False, |
| keystore=None, |
| install=False, |
| quiet=False, |
| logging=True): |
| if not out: |
| out = os.path.basename(apk) |
| if not keystore: |
| keystore = apk_utils.default_keystore() |
| with utils.TempDir() as temp: |
| processed_apk = None |
| if dex or clear_profile or has_wrong_compression(apk, compress_dex): |
| processed_apk = repack(apk, clear_profile, dex, |
| desugared_library_dex, compress_dex, |
| resources, temp, quiet, logging) |
| else: |
| assert not desugared_library_dex |
| utils.Print('Signing original APK without modifying apk', |
| quiet=quiet) |
| processed_apk = os.path.join(temp, 'processed.apk') |
| shutil.copyfile(apk, processed_apk) |
| if sign_before_align: |
| signed_apk = sign(processed_apk, |
| keystore, |
| temp, |
| quiet=quiet, |
| logging=logging) |
| aligned_apk = align(signed_apk, temp, quiet=quiet, logging=logging) |
| utils.Print('Writing result to {}'.format(out), quiet=quiet) |
| shutil.copyfile(aligned_apk, out) |
| else: |
| aligned_apk = align(processed_apk, |
| temp, |
| quiet=quiet, |
| logging=logging) |
| signed_apk = sign(aligned_apk, |
| keystore, |
| temp, |
| quiet=quiet, |
| logging=logging) |
| utils.Print('Writing result to {}'.format(out), quiet=quiet) |
| shutil.copyfile(signed_apk, out) |
| if install: |
| adb_cmd = ['adb'] |
| if adb_options: |
| adb_cmd.extend( |
| [option for option in adb_options.split(' ') if option]) |
| adb_cmd.extend(['install', '-t', '-r', '-d', out]) |
| utils.RunCmd(adb_cmd, quiet=quiet, logging=logging) |
| |
| |
| def main(): |
| (options, apks) = parse_options() |
| if len(apks) == 1: |
| masseur(apks[0], **vars(options)) |
| else: |
| for apk in apks: |
| print(f'Processing {apk}') |
| masseur(apk, **vars(options)) |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |