|  | #!/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()) |