blob: 770a6512b1c35ba72236ca3e5138b018f10319e1 [file] [log] [blame]
Søren Gjessecdae8792018-12-12 09:02:43 +01001#!/usr/bin/env python
2# Copyright (c) 2018, 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
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +01006import apk_masseur
Søren Gjessecdae8792018-12-12 09:02:43 +01007import apk_utils
Søren Gjesseeed839d2019-01-11 15:19:16 +01008import gradle
Søren Gjessecdae8792018-12-12 09:02:43 +01009import os
10import optparse
11import subprocess
12import sys
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +010013import tempfile
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010014import time
Søren Gjessecdae8792018-12-12 09:02:43 +010015import utils
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +010016import zipfile
Søren Gjessecdae8792018-12-12 09:02:43 +010017
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010018import as_utils
19
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +010020SHRINKERS = ['r8', 'r8-minified', 'r8full', 'r8full-minified', 'proguard']
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010021WORKING_DIR = utils.BUILD
22
23if 'R8_BENCHMARK_DIR' in os.environ and os.path.isdir(os.environ['R8_BENCHMARK_DIR']):
24 WORKING_DIR = os.environ['R8_BENCHMARK_DIR']
Søren Gjessecdae8792018-12-12 09:02:43 +010025
26APPS = {
27 # 'app-name': {
28 # 'git_repo': ...
29 # 'app_module': ... (default app)
30 # 'archives_base_name': ... (default same as app_module)
31 # 'flavor': ... (default no flavor)
Søren Gjesse8c111482018-12-21 14:47:57 +010032 # 'releaseTarget': ... (default <app_module>:assemble<flavor>Release
Søren Gjessecdae8792018-12-12 09:02:43 +010033 # },
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010034 'AnExplorer': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010035 'app_id': 'dev.dworks.apps.anexplorer.pro',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010036 'git_repo': 'https://github.com/1hakr/AnExplorer',
37 'flavor': 'googleMobilePro',
38 'signed-apk-name': 'AnExplorer-googleMobileProRelease-4.0.3.apk',
39 },
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010040 'AntennaPod': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010041 'app_id': 'de.danoeh.antennapod',
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010042 'git_repo': 'https://github.com/AntennaPod/AntennaPod.git',
43 'flavor': 'play',
44 },
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010045 'apps-android-wikipedia': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010046 'app_id': 'org.wikipedia',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010047 'git_repo': 'https://github.com/wikimedia/apps-android-wikipedia',
48 'flavor': 'prod',
49 'signed-apk-name': 'app-prod-universal-release.apk'
50 },
Christoffer Quist Adamsen61b8c802018-12-20 08:18:40 +010051 'friendlyeats-android': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010052 'app_id': 'com.google.firebase.example.fireeats',
Christoffer Quist Adamsen61b8c802018-12-20 08:18:40 +010053 'git_repo': 'https://github.com/firebase/friendlyeats-android.git'
54 },
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010055 'KISS': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010056 'app_id': 'fr.neamar.kiss',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010057 'git_repo': 'https://github.com/Neamar/KISS',
58 },
59 'materialistic': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010060 'app_id': 'io.github.hidroh.materialistic',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010061 'git_repo': 'https://github.com/hidroh/materialistic',
62 },
63 'Minimal-Todo': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010064 'app_id': 'com.avjindersinghsekhon.minimaltodo',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010065 'git_repo': 'https://github.com/avjinder/Minimal-Todo',
66 },
67 'NewPipe': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010068 'app_id': 'org.schabi.newpipe',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010069 'git_repo': 'https://github.com/TeamNewPipe/NewPipe',
70 },
71 'Simple-Calendar': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010072 'app_id': 'com.simplemobiletools.calendar.pro',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010073 'git_repo': 'https://github.com/SimpleMobileTools/Simple-Calendar',
74 'signed-apk-name': 'calendar-release.apk'
75 },
Søren Gjessecdae8792018-12-12 09:02:43 +010076 'tachiyomi': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010077 'app_id': 'eu.kanade.tachiyomi',
Søren Gjessecdae8792018-12-12 09:02:43 +010078 'git_repo': 'https://github.com/sgjesse/tachiyomi.git',
79 'flavor': 'standard',
Søren Gjesse8c111482018-12-21 14:47:57 +010080 'releaseTarget': 'app:assembleRelease',
Søren Gjessecdae8792018-12-12 09:02:43 +010081 },
82 # This does not build yet.
83 'muzei': {
84 'git_repo': 'https://github.com/sgjesse/muzei.git',
85 'app_module': 'main',
86 'archives_base_name': 'muzei',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010087 'skip': True,
Søren Gjessecdae8792018-12-12 09:02:43 +010088 },
89}
90
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010091# Common environment setup.
92user_home = os.path.expanduser('~')
93android_home = os.path.join(user_home, 'Android', 'Sdk')
94android_build_tools_version = '28.0.3'
95android_build_tools = os.path.join(
96 android_home, 'build-tools', android_build_tools_version)
97
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010098# TODO(christofferqa): Do not rely on 'emulator-5554' name
99emulator_id = 'emulator-5554'
100
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100101def ComputeSizeOfDexFilesInApk(apk):
102 dex_size = 0
103 z = zipfile.ZipFile(apk, 'r')
104 for filename in z.namelist():
105 if filename.endswith('.dex'):
106 dex_size += z.getinfo(filename).file_size
107 return dex_size
108
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +0100109def IsBuiltWithR8(apk):
110 script = os.path.join(utils.TOOLS_DIR, 'extractmarker.py')
111 return '~~R8' in subprocess.check_output(['python', script, apk]).strip()
112
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100113def IsMinifiedR8(shrinker):
114 return shrinker == 'r8-minified' or shrinker == 'r8full-minified'
115
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +0100116def IsTrackedByGit(file):
117 return subprocess.check_output(['git', 'ls-files', file]).strip() != ''
118
Søren Gjessecdae8792018-12-12 09:02:43 +0100119def GitClone(git_url):
120 return subprocess.check_output(['git', 'clone', git_url]).strip()
121
122def GitPull():
Christoffer Quist Adamsen01c7f0b2019-01-10 10:59:16 +0100123 # Use --no-edit to accept the auto-generated merge message, if any.
124 return subprocess.call(['git', 'pull', '--no-edit']) == 0
Søren Gjessecdae8792018-12-12 09:02:43 +0100125
126def GitCheckout(file):
127 return subprocess.check_output(['git', 'checkout', file]).strip()
128
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100129def InstallApkOnEmulator(apk_dest):
130 subprocess.check_call(
131 ['adb', '-s', emulator_id, 'install', '-r', '-d', apk_dest])
132
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100133def PercentageDiffAsString(before, after):
134 if after < before:
135 return '-' + str(round((1.0 - after / before) * 100)) + '%'
136 else:
137 return '+' + str(round((after - before) / before * 100)) + '%'
138
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100139def UninstallApkOnEmulator(app, config):
140 app_id = config.get('app_id')
141 process = subprocess.Popen(
142 ['adb', 'uninstall', app_id],
143 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
144 stdout, stderr = process.communicate()
145
146 if stdout.strip() == 'Success':
147 # Successfully uninstalled
148 return
149
150 if 'Unknown package: {}'.format(app_id) in stderr:
151 # Application not installed
152 return
153
154 raise Exception(
155 'Unexpected result from `adb uninstall {}\nStdout: {}\nStderr: {}'.format(
156 app_id, stdout, stderr))
157
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100158def WaitForEmulator():
159 stdout = subprocess.check_output(['adb', 'devices'])
160 if '{}\tdevice'.format(emulator_id) in stdout:
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100161 return True
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100162
163 print('Emulator \'{}\' not connected; waiting for connection'.format(
164 emulator_id))
165
166 time_waited = 0
167 while True:
168 time.sleep(10)
169 time_waited += 10
170 stdout = subprocess.check_output(['adb', 'devices'])
171 if '{}\tdevice'.format(emulator_id) not in stdout:
172 print('... still waiting for connection')
173 if time_waited >= 5 * 60:
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100174 return False
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100175 else:
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100176 return True
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100177
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100178def GetResultsForApp(app, config, options):
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100179 git_repo = config['git_repo']
180
181 # Checkout and build in the build directory.
182 checkout_dir = os.path.join(WORKING_DIR, app)
183
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100184 result = {}
185
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100186 if not os.path.exists(checkout_dir):
187 with utils.ChangedWorkingDirectory(WORKING_DIR):
188 GitClone(git_repo)
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100189 elif options.pull:
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100190 with utils.ChangedWorkingDirectory(checkout_dir):
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100191 # Checkout build.gradle to avoid merge conflicts.
192 if IsTrackedByGit('build.gradle'):
193 GitCheckout('build.gradle')
194
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100195 if not GitPull():
196 result['status'] = 'failed'
197 result['error_message'] = 'Unable to pull from remote'
198 return result
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100199
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100200 result['status'] = 'success'
201
202 result_per_shrinker = BuildAppWithSelectedShrinkers(
203 app, config, options, checkout_dir)
204 for shrinker, shrinker_result in result_per_shrinker.iteritems():
205 result[shrinker] = shrinker_result
206
207 return result
208
209def BuildAppWithSelectedShrinkers(app, config, options, checkout_dir):
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100210 result_per_shrinker = {}
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100211
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100212 with utils.ChangedWorkingDirectory(checkout_dir):
213 for shrinker in SHRINKERS:
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100214 if options.shrinker and shrinker not in options.shrinker:
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100215 continue
216
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100217 apk_dest = None
218 result = {}
219 try:
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100220 (apk_dest, profile_dest_dir, proguard_config_file) = \
221 BuildAppWithShrinker(app, config, shrinker, checkout_dir, options)
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100222 dex_size = ComputeSizeOfDexFilesInApk(apk_dest)
223 result['apk_dest'] = apk_dest,
224 result['build_status'] = 'success'
225 result['dex_size'] = dex_size
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100226 result['profile_dest_dir'] = profile_dest_dir
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100227
228 profile = as_utils.ParseProfileReport(profile_dest_dir)
229 result['profile'] = {
230 task_name:duration for task_name, duration in profile.iteritems()
231 if as_utils.IsGradleCompilerTask(task_name, shrinker)}
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100232 except Exception as e:
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100233 warn('Failed to build {} with {}'.format(app, shrinker))
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100234 if e:
235 print('Error: ' + str(e))
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100236 result['build_status'] = 'failed'
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100237
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100238 if result.get('build_status') == 'success':
239 if options.monkey:
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100240 result['monkey_status'] = 'success' if RunMonkey(
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100241 app, config, options, apk_dest) else 'failed'
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100242
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100243 if 'r8' in shrinker and options.r8_compilation_steps > 1:
244 recompilation_results = []
245 previous_apk = apk_dest
246 for i in range(1, options.r8_compilation_steps):
247 try:
248 recompiled_apk_dest = os.path.join(
249 checkout_dir, 'out', shrinker, 'app-release-{}.apk'.format(i))
250 RebuildAppWithShrinker(
251 previous_apk, recompiled_apk_dest, proguard_config_file, shrinker)
252 recompilation_result = {
253 'apk_dest': recompiled_apk_dest,
254 'build_status': 'success',
255 'dex_size': ComputeSizeOfDexFilesInApk(recompiled_apk_dest)
256 }
257 if options.monkey:
258 recompilation_result['monkey_status'] = 'success' if RunMonkey(
259 app, config, options, recompiled_apk_dest) else 'failed'
260 recompilation_results.append(recompilation_result)
261 previous_apk = recompiled_apk_dest
262 except Exception as e:
263 warn('Failed to recompile {} with {}'.format(app, shrinker))
264 recompilation_results.append({ 'build_status': 'failed' })
265 break
266 result['recompilation_results'] = recompilation_results
267
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100268 result_per_shrinker[shrinker] = result
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100269
270 if IsTrackedByGit('gradle.properties'):
271 GitCheckout('gradle.properties')
272
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100273 return result_per_shrinker
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100274
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100275def BuildAppWithShrinker(app, config, shrinker, checkout_dir, options):
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100276 print()
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100277 print('Building {} with {}'.format(app, shrinker))
278
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100279 # Add/remove 'r8.jar' from top-level build.gradle.
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100280 if options.disable_tot:
281 as_utils.remove_r8_dependency(checkout_dir)
282 else:
283 as_utils.add_r8_dependency(checkout_dir, IsMinifiedR8(shrinker))
284
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100285 app_module = config.get('app_module', 'app')
286 archives_base_name = config.get(' archives_base_name', app_module)
287 flavor = config.get('flavor')
288
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100289 # Ensure that gradle.properties is not modified before modifying it to
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100290 # select shrinker.
291 if IsTrackedByGit('gradle.properties'):
292 GitCheckout('gradle.properties')
293 with open("gradle.properties", "a") as gradle_properties:
294 if 'r8' in shrinker:
295 gradle_properties.write('\nandroid.enableR8=true\n')
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100296 if shrinker == 'r8full' or shrinker == 'r8full-minified':
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100297 gradle_properties.write('android.enableR8.fullMode=true\n')
298 else:
299 assert shrinker == 'proguard'
300 gradle_properties.write('\nandroid.enableR8=false\n')
301
302 out = os.path.join(checkout_dir, 'out', shrinker)
303 if not os.path.exists(out):
304 os.makedirs(out)
305
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100306 # Set -printconfiguration in Proguard rules.
307 proguard_config_dest = os.path.abspath(
308 os.path.join(out, 'proguard-rules.pro'))
309 as_utils.SetPrintConfigurationDirective(
310 app, config, checkout_dir, proguard_config_dest)
311
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100312 env = os.environ.copy()
313 env['ANDROID_HOME'] = android_home
314 env['JAVA_OPTS'] = '-ea'
Søren Gjesse8c111482018-12-21 14:47:57 +0100315 releaseTarget = config.get('releaseTarget')
316 if not releaseTarget:
317 releaseTarget = app_module + ':' + 'assemble' + (
318 flavor.capitalize() if flavor else '') + 'Release'
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100319
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100320 cmd = ['./gradlew', '--no-daemon', 'clean', releaseTarget, '--profile',
321 '--stacktrace']
322
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100323 utils.PrintCmd(cmd)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100324 build_process = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE)
325 stdout = []
326 while True:
327 line = build_process.stdout.readline()
328 if line != b'':
329 stripped = line.rstrip()
330 stdout.append(stripped)
331 print(stripped)
332 else:
333 break
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100334
335 apk_base_name = (archives_base_name
336 + (('-' + flavor) if flavor else '') + '-release')
337 signed_apk_name = config.get('signed-apk-name', apk_base_name + '.apk')
338 unsigned_apk_name = apk_base_name + '-unsigned.apk'
339
340 build_dir = config.get('build_dir', 'build')
341 build_output_apks = os.path.join(app_module, build_dir, 'outputs', 'apk')
342 if flavor:
343 build_output_apks = os.path.join(build_output_apks, flavor, 'release')
344 else:
345 build_output_apks = os.path.join(build_output_apks, 'release')
346
347 signed_apk = os.path.join(build_output_apks, signed_apk_name)
348 unsigned_apk = os.path.join(build_output_apks, unsigned_apk_name)
349
350 if options.sign_apks and not os.path.isfile(signed_apk):
351 assert os.path.isfile(unsigned_apk)
352 if options.sign_apks:
353 keystore = 'app.keystore'
354 keystore_password = 'android'
355 apk_utils.sign_with_apksigner(
356 android_build_tools,
357 unsigned_apk,
358 signed_apk,
359 keystore,
360 keystore_password)
361
362 if os.path.isfile(signed_apk):
363 apk_dest = os.path.join(out, signed_apk_name)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100364 as_utils.MoveFile(signed_apk, apk_dest)
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100365 else:
366 apk_dest = os.path.join(out, unsigned_apk_name)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100367 as_utils.MoveFile(unsigned_apk, apk_dest)
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100368
369 assert IsBuiltWithR8(apk_dest) == ('r8' in shrinker), (
370 'Unexpected marker in generated APK for {}'.format(shrinker))
371
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100372 profile_dest_dir = os.path.join(out, 'profile')
373 as_utils.MoveProfileReportTo(profile_dest_dir, stdout)
374
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100375 return (apk_dest, profile_dest_dir, proguard_config_dest)
376
377def RebuildAppWithShrinker(apk, apk_dest, proguard_config_file, shrinker):
378 assert 'r8' in shrinker
379 assert apk_dest.endswith('.apk')
380
381 # Compile given APK with shrinker to temporary zip file.
382 api = 28 # TODO(christofferqa): Should be the one from build.gradle
383 android_jar = os.path.join(utils.REPO_ROOT, utils.ANDROID_JAR.format(api=api))
384 r8_jar = utils.R8LIB_JAR if IsMinifiedR8(shrinker) else utils.R8_JAR
385 zip_dest = apk_dest[:-3] + '.zip'
386
387 cmd = ['java', '-ea', '-jar', r8_jar, '--release', '--pg-conf',
388 proguard_config_file, '--lib', android_jar, '--output', zip_dest, apk]
389 utils.PrintCmd(cmd)
390
391 subprocess.check_output(cmd)
392
393 # Make a copy of the given APK, move the newly generated dex files into the
394 # copied APK, and then sign the APK.
395 apk_masseur.masseur(apk, dex=zip_dest, out=apk_dest)
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100396
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100397def RunMonkey(app, config, options, apk_dest):
398 if not WaitForEmulator():
399 return False
400
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100401 UninstallApkOnEmulator(app, config)
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100402 InstallApkOnEmulator(apk_dest)
403
404 app_id = config.get('app_id')
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100405 number_of_events_to_generate = options.monkey_events
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100406
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100407 # Intentionally using a constant seed such that the monkey generates the same
408 # event sequence for each shrinker.
409 random_seed = 42
410
411 cmd = ['adb', 'shell', 'monkey', '-p', app_id, '-s', str(random_seed),
Christoffer Quist Adamsen88b7ebe2019-01-14 11:22:17 +0100412 str(number_of_events_to_generate)]
413 utils.PrintCmd(cmd)
414
415 try:
416 stdout = subprocess.check_output(cmd)
417 except subprocess.CalledProcessError as e:
418 return False
419
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100420 return 'Events injected: {}'.format(number_of_events_to_generate) in stdout
421
422def LogResults(result_per_shrinker_per_app, options):
423 for app, result_per_shrinker in result_per_shrinker_per_app.iteritems():
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100424 print(app + ':')
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100425
426 if result_per_shrinker.get('status') != 'success':
427 error_message = result_per_shrinker.get('error_message')
428 print(' skipped ({})'.format(error_message))
429 continue
430
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100431 proguard_result = result_per_shrinker.get('proguard', {})
432 proguard_dex_size = float(proguard_result.get('dex_size', -1))
433 proguard_duration = sum(proguard_result.get('profile', {}).values())
434
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100435 for shrinker in SHRINKERS:
436 if shrinker not in result_per_shrinker:
437 continue
438 result = result_per_shrinker.get(shrinker)
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100439 build_status = result.get('build_status')
440 if build_status != 'success':
441 warn(' {}: {}'.format(shrinker, build_status))
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100442 else:
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100443 print(' {}:'.format(shrinker))
444 dex_size = result.get('dex_size')
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100445 msg = ' dex size: {}'.format(dex_size)
446 if dex_size != proguard_dex_size and proguard_dex_size >= 0:
447 msg = '{} ({}, {})'.format(
448 msg, dex_size - proguard_dex_size,
449 PercentageDiffAsString(proguard_dex_size, dex_size))
450 success(msg) if dex_size < proguard_dex_size else warn(msg)
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100451 else:
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100452 print(msg)
453
454 profile = result.get('profile')
455 duration = sum(profile.values())
456 msg = ' performance: {}s'.format(duration)
457 if duration != proguard_duration and proguard_duration > 0:
458 msg = '{} ({}s, {})'.format(
459 msg, duration - proguard_duration,
460 PercentageDiffAsString(proguard_duration, duration))
461 success(msg) if duration < proguard_duration else warn(msg)
462 else:
463 print(msg)
464 if len(profile) >= 2:
465 for task_name, task_duration in profile.iteritems():
466 print(' {}: {}s'.format(task_name, task_duration))
467
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100468 if options.monkey:
469 monkey_status = result.get('monkey_status')
470 if monkey_status != 'success':
471 warn(' monkey: {}'.format(monkey_status))
472 else:
473 success(' monkey: {}'.format(monkey_status))
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100474 recompilation_results = result.get('recompilation_results', [])
475 i = 1
476 for recompilation_result in recompilation_results:
477 build_status = recompilation_result.get('build_status')
478 if build_status != 'success':
479 print(' recompilation #{}: {}'.format(i, build_status))
480 else:
481 dex_size = recompilation_result.get('dex_size')
482 print(' recompilation #{}'.format(i))
483 print(' dex size: {}'.format(dex_size))
484 if options.monkey:
485 monkey_status = recompilation_result.get('monkey_status')
486 msg = ' monkey: {}'.format(monkey_status)
487 success(msg) if monkey_status == 'success' else warn(msg)
488 i += 1
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100489
Søren Gjessecdae8792018-12-12 09:02:43 +0100490def ParseOptions(argv):
491 result = optparse.OptionParser()
492 result.add_option('--app',
493 help='What app to run on',
494 choices=APPS.keys())
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100495 result.add_option('--monkey',
496 help='Whether to install and run app(s) with monkey',
497 default=False,
498 action='store_true')
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100499 result.add_option('--monkey_events',
500 help='Number of events that the monkey should trigger',
501 default=250,
502 type=int)
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100503 result.add_option('--pull',
504 help='Whether to pull the latest version of each app',
505 default=False,
506 action='store_true')
Søren Gjesseeed839d2019-01-11 15:19:16 +0100507 result.add_option('--sign-apks', '--sign_apks',
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +0100508 help='Whether the APKs should be signed',
509 default=False,
510 action='store_true')
511 result.add_option('--shrinker',
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100512 help='The shrinkers to use (by default, all are run)',
513 action='append')
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100514 result.add_option('--r8-compilation-steps', '--r8_compilation_steps',
515 help='Number of times R8 should be run on each app',
516 default=2,
517 type=int)
Søren Gjesseeed839d2019-01-11 15:19:16 +0100518 result.add_option('--disable-tot', '--disable_tot',
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100519 help='Whether to disable the use of the ToT version of R8',
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100520 default=False,
521 action='store_true')
Søren Gjesseeed839d2019-01-11 15:19:16 +0100522 result.add_option('--no-build', '--no_build',
523 help='Run without building ToT first (only when using ToT)',
524 default=False,
525 action='store_true')
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100526 (options, args) = result.parse_args(argv)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100527 if options.disable_tot:
528 # r8.jar is required for recompiling the generated APK
529 options.r8_compilation_steps = 1
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100530 if options.shrinker:
531 for shrinker in options.shrinker:
532 assert shrinker in SHRINKERS
533 return (options, args)
Søren Gjessecdae8792018-12-12 09:02:43 +0100534
535def main(argv):
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100536 global SHRINKERS
537
Søren Gjessecdae8792018-12-12 09:02:43 +0100538 (options, args) = ParseOptions(argv)
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100539 assert options.disable_tot or os.path.isfile(utils.R8_JAR), (
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +0100540 'Cannot build from ToT without r8.jar')
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100541 assert options.disable_tot or os.path.isfile(utils.R8LIB_JAR), (
542 'Cannot build from ToT without r8lib.jar')
543
544 if options.disable_tot:
545 # Cannot run r8 lib without adding r8lib.jar as an dependency
546 SHRINKERS = [
547 shrinker for shrinker in SHRINKERS
548 if 'minified' not in shrinker]
Søren Gjessecdae8792018-12-12 09:02:43 +0100549
Søren Gjesseeed839d2019-01-11 15:19:16 +0100550 if not options.no_build and not options.disable_tot:
551 gradle.RunGradle(['r8', 'r8lib'])
552
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100553 result_per_shrinker_per_app = {}
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100554
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100555 if options.app:
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100556 result_per_shrinker_per_app[options.app] = GetResultsForApp(
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100557 options.app, APPS.get(options.app), options)
Søren Gjessecdae8792018-12-12 09:02:43 +0100558 else:
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100559 for app, config in APPS.iteritems():
560 if not config.get('skip', False):
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100561 result_per_shrinker_per_app[app] = GetResultsForApp(
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100562 app, config, options)
563
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100564 LogResults(result_per_shrinker_per_app, options)
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100565
566def success(message):
567 CGREEN = '\033[32m'
568 CEND = '\033[0m'
569 print(CGREEN + message + CEND)
570
571def warn(message):
572 CRED = '\033[91m'
573 CEND = '\033[0m'
574 print(CRED + message + CEND)
Søren Gjessecdae8792018-12-12 09:02:43 +0100575
576if __name__ == '__main__':
577 sys.exit(main(sys.argv[1:]))