1#!/usr/bin/env python 2# Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file 3# for details. All rights reserved. Use of this source code is governed by a 4# BSD-style license that can be found in the LICENSE file. 5 6import glob 7import optparse 8import os 9import shutil 10import subprocess 11import sys 12import utils 13 14USAGE = 'usage: %prog [options] <apk>' 15 16def parse_options(): 17 parser = optparse.OptionParser(usage=USAGE) 18 parser.add_option('--dex', 19 help='directory with dex files to use instead of those in the apk', 20 default=None) 21 parser.add_option('--out', 22 help='output file (default ./$(basename <apk>))', 23 default=None) 24 parser.add_option('--keystore', 25 help='keystore file (default ~/.android/app.keystore)', 26 default=None) 27 parser.add_option('--install', 28 help='install the generated apk with adb options -t -r -d', 29 default=False, 30 action='store_true') 31 parser.add_option('--adb-options', 32 help='additional adb options when running adb', 33 default=None) 34 (options, args) = parser.parse_args() 35 if len(args) != 1: 36 parser.error('Expected <apk> argument, got: ' + ' '.join(args)) 37 apk = args[0] 38 if not options.out: 39 options.out = os.path.basename(apk) 40 if not options.keystore: 41 options.keystore = findKeystore() 42 return (options, apk) 43 44def findKeystore(): 45 return os.path.join(os.getenv('HOME'), '.android', 'app.keystore') 46 47def repack(processed_out, original_apk, temp): 48 processed_apk = os.path.join(temp, 'processed.apk') 49 shutil.copyfile(original_apk, processed_apk) 50 if not processed_out: 51 print 'Using original APK as is' 52 return processed_apk 53 print 'Repacking APK with dex files from', processed_apk 54 with utils.ChangedWorkingDirectory(temp): 55 cmd = ['zip', '-d', 'processed.apk', '*.dex'] 56 utils.PrintCmd(cmd) 57 subprocess.check_call(cmd) 58 if processed_out.endswith('.zip') or processed_out.endswith('.jar'): 59 cmd = ['unzip', processed_out, '-d', temp] 60 utils.PrintCmd(cmd) 61 subprocess.check_call(cmd) 62 processed_out = temp 63 with utils.ChangedWorkingDirectory(processed_out): 64 dex = glob.glob('*.dex') 65 cmd = ['zip', '-u', '-9', processed_apk] + dex 66 utils.PrintCmd(cmd) 67 subprocess.check_call(cmd) 68 return processed_apk 69 70def sign(unsigned_apk, keystore, temp): 71 print 'Signing (ignore the warnings)' 72 cmd = ['zip', '-d', unsigned_apk, 'META-INF/*'] 73 utils.PrintCmd(cmd) 74 subprocess.call(cmd) 75 signed_apk = os.path.join(temp, 'unaligned.apk') 76 cmd = [ 77 'jarsigner', 78 '-sigalg', 'SHA1withRSA', 79 '-digestalg', 'SHA1', 80 '-keystore', keystore, 81 '-storepass', 'android', 82 '-signedjar', signed_apk, 83 unsigned_apk, 84 'androiddebugkey' 85 ] 86 utils.PrintCmd(cmd) 87 subprocess.check_call(cmd) 88 return signed_apk 89 90def align(signed_apk, temp): 91 print 'Aligning' 92 aligned_apk = os.path.join(temp, 'aligned.apk') 93 cmd = ['zipalign', '-f', '4', signed_apk, aligned_apk] 94 print ' '.join(cmd) 95 subprocess.check_call(cmd) 96 return signed_apk 97 98def main(): 99 (options, apk) = parse_options() 100 with utils.TempDir() as temp: 101 processed_apk = None 102 if options.dex: 103 processed_apk = repack(options.dex, apk, temp) 104 else: 105 print 'Signing original APK without modifying dex files' 106 processed_apk = os.path.join(temp, 'processed.apk') 107 shutil.copyfile(apk, processed_apk) 108 signed_apk = sign(processed_apk, options.keystore, temp) 109 aligned_apk = align(signed_apk, temp) 110 print 'Writing result to', options.out 111 shutil.copyfile(aligned_apk, options.out) 112 adb_cmd = ['adb'] 113 if options.adb_options: 114 adb_cmd.extend( 115 [option for option in options.adb_options.split(' ') if option]) 116 if options.install: 117 adb_cmd.extend(['install', '-t', '-r', '-d', options.out]); 118 utils.PrintCmd(adb_cmd) 119 subprocess.check_call(adb_cmd) 120 return 0 121 122if __name__ == '__main__': 123 sys.exit(main()) 124