blob: 4d92044ddb2d6373e6e33aa604ccf8bfcdd9d3f3 [file] [log] [blame]
Ian Zernydcb172e2022-02-22 15:36:45 +01001#!/usr/bin/env python3
Mads Ager418d1ca2017-05-22 09:35:49 +02002# 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
Christoffer Adamsen41b12442024-07-03 12:24:16 +020010import subprocess
Mads Ager418d1ca2017-05-22 09:35:49 +020011import sys
Christoffer Quist Adamsen0aaca162023-02-27 13:02:01 +010012
13import apk_utils
Mads Ager418d1ca2017-05-22 09:35:49 +020014import utils
Christoffer Quist Adamsen0aaca162023-02-27 13:02:01 +010015import zip_utils
Mads Ager418d1ca2017-05-22 09:35:49 +020016
17USAGE = 'usage: %prog [options] <apk>'
18
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020019
Mads Ager418d1ca2017-05-22 09:35:49 +020020def parse_options():
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020021 parser = optparse.OptionParser(usage=USAGE)
22 parser.add_option('--clear-profile',
23 help='To remove baseline.prof and baseline.profm from '
24 'assets/dexopt/',
25 default=False,
26 action='store_true')
Christoffer Adamsen456ea7f2024-07-01 15:00:29 +020027 parser.add_option('--compress-dex',
28 help='Whether the dex should be stored compressed',
29 action='store_true',
30 default=False)
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020031 parser.add_option('--dex',
32 help='Directory or archive with dex files to use instead '
33 'of those in the apk',
34 default=None)
35 parser.add_option(
36 '--desugared-library-dex',
37 help='Path to desugared library dex file to use or archive '
38 'containing a single classes.dex file',
39 default=None)
40 parser.add_option(
41 '--resources',
42 help=('pattern that matches resources to use instead of ' +
43 'those in the apk'),
44 default=None)
45 parser.add_option('--out',
46 help='output file (default ./$(basename <apk>))',
47 default=None)
48 parser.add_option('--keystore',
49 help='keystore file (default ~/.android/app.keystore)',
50 default=None)
51 parser.add_option(
52 '--install',
53 help='install the generated apk with adb options -t -r -d',
54 default=False,
55 action='store_true')
56 parser.add_option('--adb-options',
57 help='additional adb options when running adb',
58 default=None)
59 parser.add_option('--quiet', help='disable verbose logging', default=False)
60 parser.add_option('--sign-before-align',
61 help='Sign the apk before aligning',
62 default=False,
63 action='store_true')
Christoffer Adamsenb9f334e2024-07-03 12:51:23 +020064 (options, apks) = parser.parse_args()
65 if len(apks) == 0:
66 parser.error('Expected one or more apk arguments, got none.')
67 if len(apks) > 1 and options.out:
68 parser.error('Cannot process multiple apks with --out')
69 return (options, apks)
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020070
Mads Ager418d1ca2017-05-22 09:35:49 +020071
Christoffer Quist Adamsen0aaca162023-02-27 13:02:01 +010072def is_archive(file):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020073 return file.endswith('.zip') or file.endswith('.jar')
Christoffer Quist Adamsen0aaca162023-02-27 13:02:01 +010074
Christoffer Quist Adamsen632c9552023-02-27 17:34:23 +010075
Christoffer Adamsen456ea7f2024-07-01 15:00:29 +020076def repack(apk, clear_profile, processed_out, desugared_library_dex,
77 compress_dex, resources, temp, quiet, logging):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020078 processed_apk = os.path.join(temp, 'processed.apk')
79 shutil.copyfile(apk, processed_apk)
Christoffer Quist Adamsen632c9552023-02-27 17:34:23 +010080
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020081 if clear_profile:
82 zip_utils.remove_files_from_zip(
83 ['assets/dexopt/baseline.prof', 'assets/dexopt/baseline.profm'],
84 processed_apk)
85
86 if not processed_out:
Christoffer Adamsen41b12442024-07-03 12:24:16 +020087 if has_wrong_compression(apk, compress_dex):
88 processed_out = os.path.join(temp, 'extracted')
89 subprocess.check_call(
90 ['unzip', apk, '-d', processed_out, 'classes*.dex'])
91 else:
92 utils.Print('Using original dex as is', quiet=quiet)
93 return processed_apk
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020094
95 utils.Print('Repacking APK with dex files from {}'.format(processed_out),
96 quiet=quiet)
97
98 # Delete original dex files in APK.
99 with utils.ChangedWorkingDirectory(temp, quiet=quiet):
100 cmd = ['zip', '-d', 'processed.apk', '*.dex']
101 utils.RunCmd(cmd, quiet=quiet, logging=logging)
102
103 # Unzip the jar or zip file into `temp`.
104 if is_archive(processed_out):
105 cmd = ['unzip', processed_out, '-d', temp]
106 if quiet:
107 cmd.insert(1, '-q')
108 utils.RunCmd(cmd, quiet=quiet, logging=logging)
109 processed_out = temp
110 elif desugared_library_dex:
111 for dex_name in glob.glob('*.dex', root_dir=processed_out):
112 src = os.path.join(processed_out, dex_name)
113 dst = os.path.join(temp, dex_name)
114 shutil.copyfile(src, dst)
115 processed_out = temp
116
117 if desugared_library_dex:
118 desugared_library_dex_index = len(glob.glob('*.dex', root_dir=temp)) + 1
119 desugared_library_dex_name = 'classes%s.dex' % desugared_library_dex_index
120 desugared_library_dex_dst = os.path.join(temp,
121 desugared_library_dex_name)
122 if is_archive(desugared_library_dex):
123 zip_utils.extract_member(desugared_library_dex, 'classes.dex',
124 desugared_library_dex_dst)
125 else:
126 shutil.copyfile(desugared_library_dex, desugared_library_dex_dst)
127
128 # Insert the new dex and resource files from `processed_out` into the APK.
129 with utils.ChangedWorkingDirectory(processed_out, quiet=quiet):
130 dex_files = glob.glob('*.dex')
131 dex_files.sort()
132 resource_files = glob.glob(resources) if resources else []
Christoffer Adamsen456ea7f2024-07-01 15:00:29 +0200133 cmd = ['zip', '-u']
134 if not compress_dex:
135 cmd.append('-0')
136 cmd.append(processed_apk)
137 cmd.extend(dex_files)
138 cmd.extend(resource_files)
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200139 utils.RunCmd(cmd, quiet=quiet, logging=logging)
Mads Ager418d1ca2017-05-22 09:35:49 +0200140 return processed_apk
Christoffer Quist Adamsen632c9552023-02-27 17:34:23 +0100141
Mads Ager418d1ca2017-05-22 09:35:49 +0200142
Christoffer Quist Adamsen41cbdca2019-04-12 08:52:03 +0200143def sign(unsigned_apk, keystore, temp, quiet, logging):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200144 signed_apk = os.path.join(temp, 'unaligned.apk')
145 return apk_utils.sign_with_apksigner(unsigned_apk,
146 signed_apk,
147 keystore,
148 quiet=quiet,
149 logging=logging)
150
Mads Ager418d1ca2017-05-22 09:35:49 +0200151
Christoffer Quist Adamsen41cbdca2019-04-12 08:52:03 +0200152def align(signed_apk, temp, quiet, logging):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200153 utils.Print('Aligning', quiet=quiet)
154 aligned_apk = os.path.join(temp, 'aligned.apk')
155 return apk_utils.align(signed_apk, aligned_apk)
Mads Ager418d1ca2017-05-22 09:35:49 +0200156
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200157
Christoffer Adamsen41b12442024-07-03 12:24:16 +0200158def has_wrong_compression(apk, compress_dex):
159 cmd = ['zipinfo', apk, 'classes*.dex']
160 stdout = subprocess.check_output(cmd).decode('utf-8').strip()
161 expected = ' defN ' if compress_dex else ' stor '
162 for line in stdout.splitlines():
163 if not expected in line:
164 return True
165 return False
166
167
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200168def masseur(apk,
169 clear_profile=False,
170 dex=None,
171 desugared_library_dex=None,
Christoffer Adamsen456ea7f2024-07-01 15:00:29 +0200172 compress_dex=False,
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200173 resources=None,
174 out=None,
175 adb_options=None,
176 sign_before_align=False,
177 keystore=None,
178 install=False,
179 quiet=False,
180 logging=True):
181 if not out:
182 out = os.path.basename(apk)
183 if not keystore:
184 keystore = apk_utils.default_keystore()
185 with utils.TempDir() as temp:
186 processed_apk = None
Christoffer Adamsen41b12442024-07-03 12:24:16 +0200187 if dex or clear_profile or has_wrong_compression(apk, compress_dex):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200188 processed_apk = repack(apk, clear_profile, dex,
Christoffer Adamsen456ea7f2024-07-01 15:00:29 +0200189 desugared_library_dex, compress_dex,
190 resources, temp, quiet, logging)
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200191 else:
192 assert not desugared_library_dex
193 utils.Print('Signing original APK without modifying apk',
194 quiet=quiet)
195 processed_apk = os.path.join(temp, 'processed.apk')
196 shutil.copyfile(apk, processed_apk)
197 if sign_before_align:
198 signed_apk = sign(processed_apk,
199 keystore,
200 temp,
201 quiet=quiet,
202 logging=logging)
203 aligned_apk = align(signed_apk, temp, quiet=quiet, logging=logging)
204 utils.Print('Writing result to {}'.format(out), quiet=quiet)
205 shutil.copyfile(aligned_apk, out)
206 else:
207 aligned_apk = align(processed_apk,
208 temp,
209 quiet=quiet,
210 logging=logging)
211 signed_apk = sign(aligned_apk,
212 keystore,
213 temp,
214 quiet=quiet,
215 logging=logging)
216 utils.Print('Writing result to {}'.format(out), quiet=quiet)
217 shutil.copyfile(signed_apk, out)
218 if install:
219 adb_cmd = ['adb']
220 if adb_options:
221 adb_cmd.extend(
222 [option for option in adb_options.split(' ') if option])
223 adb_cmd.extend(['install', '-t', '-r', '-d', out])
224 utils.RunCmd(adb_cmd, quiet=quiet, logging=logging)
225
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100226
227def main():
Christoffer Adamsenb9f334e2024-07-03 12:51:23 +0200228 (options, apks) = parse_options()
229 if len(apks) == 1:
230 masseur(apks[0], **vars(options))
231 else:
232 for apk in apks:
233 print(f'Processing {apk}')
234 masseur(apk, **vars(options))
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200235 return 0
236
Mads Ager418d1ca2017-05-22 09:35:49 +0200237
238if __name__ == '__main__':
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200239 sys.exit(main())