blob: fcd0a761c41dfeaaf9c3fa0b717e189ddba5cd7f [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 Adamsen4e661df2019-01-22 10:03:06 +010036 'git_repo': 'https://github.com/christofferqa/AnExplorer',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010037 '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 Adamsen4e661df2019-01-22 10:03:06 +010042 'git_repo': 'https://github.com/christofferqa/AntennaPod.git',
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010043 '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 Adamsen4e661df2019-01-22 10:03:06 +010047 'git_repo': 'https://github.com/christofferqa/apps-android-wikipedia',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010048 '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 Adamsen4e661df2019-01-22 10:03:06 +010053 'git_repo': 'https://github.com/christofferqa/friendlyeats-android.git'
Christoffer Quist Adamsen61b8c802018-12-20 08:18:40 +010054 },
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 Adamsen4e661df2019-01-22 10:03:06 +010057 'git_repo': 'https://github.com/christofferqa/KISS',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010058 },
59 'materialistic': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010060 'app_id': 'io.github.hidroh.materialistic',
Christoffer Quist Adamsen4e661df2019-01-22 10:03:06 +010061 'git_repo': 'https://github.com/christofferqa/materialistic',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010062 },
63 'Minimal-Todo': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010064 'app_id': 'com.avjindersinghsekhon.minimaltodo',
Christoffer Quist Adamsen4e661df2019-01-22 10:03:06 +010065 'git_repo': 'https://github.com/christofferqa/Minimal-Todo',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010066 },
67 'NewPipe': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010068 'app_id': 'org.schabi.newpipe',
Christoffer Quist Adamsen4e661df2019-01-22 10:03:06 +010069 'git_repo': 'https://github.com/christofferqa/NewPipe',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010070 },
71 'Simple-Calendar': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010072 'app_id': 'com.simplemobiletools.calendar.pro',
Christoffer Quist Adamsen4e661df2019-01-22 10:03:06 +010073 'git_repo': 'https://github.com/christofferqa/Simple-Calendar',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010074 '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',
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +010081 'min_sdk': 16
Søren Gjessecdae8792018-12-12 09:02:43 +010082 },
Søren Gjesse9fb48802019-01-18 11:00:00 +010083 'tivi': {
84 'app_id': 'app.tivi',
85 # Forked from https://github.com/chrisbanes/tivi.git removing
86 # signingConfigs.
87 'git_repo': 'https://github.com/sgjesse/tivi.git',
88 # TODO(123047413): Fails with R8.
89 'skip': True,
90 },
Morten Krogh-Jespersen47d62ba2019-01-22 08:35:49 +010091 'Signal-Android': {
92 'app_id': 'org.thoughtcrime.securesms',
93 'app_module': '',
94 'flavor': 'play',
95 'git_repo': 'https://github.com/mkj-gram/Signal-Android.git',
96 'releaseTarget': 'assemblePlayRelease',
97 'signed-apk-name': 'Signal-play-release-4.32.7.apk',
98 },
Morten Krogh-Jespersenf4fa2002019-01-22 11:10:54 +010099 'chanu': {
100 'app_id': 'com.chanapps.four.activity',
101 'git_repo': 'https://github.com/mkj-gram/chanu.git',
102 },
Søren Gjessecdae8792018-12-12 09:02:43 +0100103 # This does not build yet.
104 'muzei': {
105 'git_repo': 'https://github.com/sgjesse/muzei.git',
106 'app_module': 'main',
107 'archives_base_name': 'muzei',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100108 'skip': True,
Søren Gjessecdae8792018-12-12 09:02:43 +0100109 },
110}
111
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100112# Common environment setup.
113user_home = os.path.expanduser('~')
114android_home = os.path.join(user_home, 'Android', 'Sdk')
115android_build_tools_version = '28.0.3'
116android_build_tools = os.path.join(
117 android_home, 'build-tools', android_build_tools_version)
118
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100119# TODO(christofferqa): Do not rely on 'emulator-5554' name
120emulator_id = 'emulator-5554'
121
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100122def ComputeSizeOfDexFilesInApk(apk):
123 dex_size = 0
124 z = zipfile.ZipFile(apk, 'r')
125 for filename in z.namelist():
126 if filename.endswith('.dex'):
127 dex_size += z.getinfo(filename).file_size
128 return dex_size
129
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +0100130def IsBuiltWithR8(apk):
131 script = os.path.join(utils.TOOLS_DIR, 'extractmarker.py')
132 return '~~R8' in subprocess.check_output(['python', script, apk]).strip()
133
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100134def IsMinifiedR8(shrinker):
135 return shrinker == 'r8-minified' or shrinker == 'r8full-minified'
136
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +0100137def IsTrackedByGit(file):
138 return subprocess.check_output(['git', 'ls-files', file]).strip() != ''
139
Søren Gjessecdae8792018-12-12 09:02:43 +0100140def GitClone(git_url):
141 return subprocess.check_output(['git', 'clone', git_url]).strip()
142
143def GitPull():
Christoffer Quist Adamsen01c7f0b2019-01-10 10:59:16 +0100144 # Use --no-edit to accept the auto-generated merge message, if any.
145 return subprocess.call(['git', 'pull', '--no-edit']) == 0
Søren Gjessecdae8792018-12-12 09:02:43 +0100146
147def GitCheckout(file):
148 return subprocess.check_output(['git', 'checkout', file]).strip()
149
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100150def InstallApkOnEmulator(apk_dest):
151 subprocess.check_call(
152 ['adb', '-s', emulator_id, 'install', '-r', '-d', apk_dest])
153
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100154def PercentageDiffAsString(before, after):
155 if after < before:
156 return '-' + str(round((1.0 - after / before) * 100)) + '%'
157 else:
158 return '+' + str(round((after - before) / before * 100)) + '%'
159
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100160def UninstallApkOnEmulator(app, config):
161 app_id = config.get('app_id')
162 process = subprocess.Popen(
163 ['adb', 'uninstall', app_id],
164 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
165 stdout, stderr = process.communicate()
166
167 if stdout.strip() == 'Success':
168 # Successfully uninstalled
169 return
170
171 if 'Unknown package: {}'.format(app_id) in stderr:
172 # Application not installed
173 return
174
175 raise Exception(
176 'Unexpected result from `adb uninstall {}\nStdout: {}\nStderr: {}'.format(
177 app_id, stdout, stderr))
178
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100179def WaitForEmulator():
180 stdout = subprocess.check_output(['adb', 'devices'])
181 if '{}\tdevice'.format(emulator_id) in stdout:
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100182 return True
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100183
184 print('Emulator \'{}\' not connected; waiting for connection'.format(
185 emulator_id))
186
187 time_waited = 0
188 while True:
189 time.sleep(10)
190 time_waited += 10
191 stdout = subprocess.check_output(['adb', 'devices'])
192 if '{}\tdevice'.format(emulator_id) not in stdout:
193 print('... still waiting for connection')
194 if time_waited >= 5 * 60:
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100195 return False
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100196 else:
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100197 return True
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100198
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100199def GetResultsForApp(app, config, options):
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100200 git_repo = config['git_repo']
201
202 # Checkout and build in the build directory.
203 checkout_dir = os.path.join(WORKING_DIR, app)
204
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100205 result = {}
206
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100207 if not os.path.exists(checkout_dir):
208 with utils.ChangedWorkingDirectory(WORKING_DIR):
209 GitClone(git_repo)
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100210 elif options.pull:
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100211 with utils.ChangedWorkingDirectory(checkout_dir):
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100212 # Checkout build.gradle to avoid merge conflicts.
213 if IsTrackedByGit('build.gradle'):
214 GitCheckout('build.gradle')
215
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100216 if not GitPull():
217 result['status'] = 'failed'
218 result['error_message'] = 'Unable to pull from remote'
219 return result
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100220
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100221 result['status'] = 'success'
222
223 result_per_shrinker = BuildAppWithSelectedShrinkers(
224 app, config, options, checkout_dir)
225 for shrinker, shrinker_result in result_per_shrinker.iteritems():
226 result[shrinker] = shrinker_result
227
228 return result
229
230def BuildAppWithSelectedShrinkers(app, config, options, checkout_dir):
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100231 result_per_shrinker = {}
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100232
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100233 with utils.ChangedWorkingDirectory(checkout_dir):
234 for shrinker in SHRINKERS:
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100235 if options.shrinker and shrinker not in options.shrinker:
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100236 continue
237
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100238 apk_dest = None
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100239
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100240 result = {}
241 try:
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100242 out_dir = os.path.join(checkout_dir, 'out', shrinker)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100243 (apk_dest, profile_dest_dir, proguard_config_file) = \
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100244 BuildAppWithShrinker(app, config, shrinker, checkout_dir, out_dir,
245 options)
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100246 dex_size = ComputeSizeOfDexFilesInApk(apk_dest)
247 result['apk_dest'] = apk_dest,
248 result['build_status'] = 'success'
249 result['dex_size'] = dex_size
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100250 result['profile_dest_dir'] = profile_dest_dir
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100251
252 profile = as_utils.ParseProfileReport(profile_dest_dir)
253 result['profile'] = {
254 task_name:duration for task_name, duration in profile.iteritems()
255 if as_utils.IsGradleCompilerTask(task_name, shrinker)}
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100256 except Exception as e:
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100257 warn('Failed to build {} with {}'.format(app, shrinker))
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100258 if e:
259 print('Error: ' + str(e))
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100260 result['build_status'] = 'failed'
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100261
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100262 if result.get('build_status') == 'success':
263 if options.monkey:
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100264 result['monkey_status'] = 'success' if RunMonkey(
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100265 app, config, options, apk_dest) else 'failed'
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100266
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100267 if 'r8' in shrinker and options.r8_compilation_steps > 1:
268 recompilation_results = []
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100269
270 # Build app with gradle using -D...keepRuleSynthesisForRecompilation=
271 # true.
272 out_dir = os.path.join(checkout_dir, 'out', shrinker + '-1')
273 extra_env_vars = {
274 'JAVA_OPTS': ' '.join([
275 '-ea:com.android.tools.r8...',
276 '-Dcom.android.tools.r8.keepRuleSynthesisForRecompilation=true'
277 ])
278 }
279 (apk_dest, profile_dest_dir, ext_proguard_config_file) = \
280 BuildAppWithShrinker(app, config, shrinker, checkout_dir, out_dir,
281 options, extra_env_vars)
282 dex_size = ComputeSizeOfDexFilesInApk(apk_dest)
283 recompilation_result = {
284 'apk_dest': apk_dest,
285 'build_status': 'success',
286 'dex_size': ComputeSizeOfDexFilesInApk(apk_dest),
287 'monkey_status': 'skipped'
288 }
289 recompilation_results.append(recompilation_result)
290
291 # Sanity check that keep rules have changed.
292 with open(ext_proguard_config_file) as new:
293 with open(proguard_config_file) as old:
Christoffer Quist Adamsend2bfcde2019-01-22 10:05:15 +0100294 assert(sum(1 for line in new) > sum(1 for line in old))
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100295
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +0100296 # Extract min-sdk and target-sdk
297 (min_sdk, compile_sdk) = as_utils.GetMinAndCompileSdk(app, config,
298 checkout_dir, apk_dest)
299
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100300 # Now rebuild generated apk.
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100301 previous_apk = apk_dest
302 for i in range(1, options.r8_compilation_steps):
303 try:
304 recompiled_apk_dest = os.path.join(
305 checkout_dir, 'out', shrinker, 'app-release-{}.apk'.format(i))
306 RebuildAppWithShrinker(
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100307 previous_apk, recompiled_apk_dest, ext_proguard_config_file,
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +0100308 shrinker, min_sdk, compile_sdk)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100309 recompilation_result = {
310 'apk_dest': recompiled_apk_dest,
311 'build_status': 'success',
312 'dex_size': ComputeSizeOfDexFilesInApk(recompiled_apk_dest)
313 }
314 if options.monkey:
315 recompilation_result['monkey_status'] = 'success' if RunMonkey(
316 app, config, options, recompiled_apk_dest) else 'failed'
317 recompilation_results.append(recompilation_result)
318 previous_apk = recompiled_apk_dest
319 except Exception as e:
320 warn('Failed to recompile {} with {}'.format(app, shrinker))
321 recompilation_results.append({ 'build_status': 'failed' })
322 break
323 result['recompilation_results'] = recompilation_results
324
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100325 result_per_shrinker[shrinker] = result
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100326
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100327 return result_per_shrinker
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100328
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100329def BuildAppWithShrinker(
330 app, config, shrinker, checkout_dir, out_dir, options, env_vars=None):
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100331 print()
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100332 print('Building {} with {}'.format(app, shrinker))
333
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100334 # Add/remove 'r8.jar' from top-level build.gradle.
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100335 if options.disable_tot:
336 as_utils.remove_r8_dependency(checkout_dir)
337 else:
338 as_utils.add_r8_dependency(checkout_dir, IsMinifiedR8(shrinker))
339
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100340 app_module = config.get('app_module', 'app')
Morten Krogh-Jespersen47d62ba2019-01-22 08:35:49 +0100341 archives_base_name = config.get('archives_base_name', app_module)
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100342 flavor = config.get('flavor')
343
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100344 if not os.path.exists(out_dir):
345 os.makedirs(out_dir)
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100346
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100347 # Set -printconfiguration in Proguard rules.
348 proguard_config_dest = os.path.abspath(
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100349 os.path.join(out_dir, 'proguard-rules.pro'))
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100350 as_utils.SetPrintConfigurationDirective(
351 app, config, checkout_dir, proguard_config_dest)
352
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100353 env = os.environ.copy()
354 env['ANDROID_HOME'] = android_home
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100355 env['JAVA_OPTS'] = '-ea:com.android.tools.r8...'
356 if env_vars:
357 env.update(env_vars)
358
Søren Gjesse8c111482018-12-21 14:47:57 +0100359 releaseTarget = config.get('releaseTarget')
360 if not releaseTarget:
361 releaseTarget = app_module + ':' + 'assemble' + (
362 flavor.capitalize() if flavor else '') + 'Release'
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100363
Søren Gjesse9fb48802019-01-18 11:00:00 +0100364 # Value for property android.enableR8.
365 enableR8 = 'r8' in shrinker
366 # Value for property android.enableR8.fullMode.
367 enableR8FullMode = shrinker == 'r8full' or shrinker == 'r8full-minified'
368 # Build gradlew command line.
369 cmd = ['./gradlew', '--no-daemon', 'clean', releaseTarget,
370 '--profile', '--stacktrace',
371 '-Pandroid.enableR8=' + str(enableR8).lower(),
372 '-Pandroid.enableR8.fullMode=' + str(enableR8FullMode).lower()]
373 if options.gradle_flags:
374 cmd.extend(options.gradle_flags.split(' '))
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100375
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100376 utils.PrintCmd(cmd)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100377 build_process = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE)
378 stdout = []
379 while True:
380 line = build_process.stdout.readline()
381 if line != b'':
382 stripped = line.rstrip()
383 stdout.append(stripped)
384 print(stripped)
385 else:
386 break
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100387
388 apk_base_name = (archives_base_name
389 + (('-' + flavor) if flavor else '') + '-release')
390 signed_apk_name = config.get('signed-apk-name', apk_base_name + '.apk')
391 unsigned_apk_name = apk_base_name + '-unsigned.apk'
392
393 build_dir = config.get('build_dir', 'build')
394 build_output_apks = os.path.join(app_module, build_dir, 'outputs', 'apk')
395 if flavor:
396 build_output_apks = os.path.join(build_output_apks, flavor, 'release')
397 else:
398 build_output_apks = os.path.join(build_output_apks, 'release')
399
400 signed_apk = os.path.join(build_output_apks, signed_apk_name)
401 unsigned_apk = os.path.join(build_output_apks, unsigned_apk_name)
402
403 if options.sign_apks and not os.path.isfile(signed_apk):
404 assert os.path.isfile(unsigned_apk)
405 if options.sign_apks:
406 keystore = 'app.keystore'
407 keystore_password = 'android'
408 apk_utils.sign_with_apksigner(
409 android_build_tools,
410 unsigned_apk,
411 signed_apk,
412 keystore,
413 keystore_password)
414
415 if os.path.isfile(signed_apk):
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100416 apk_dest = os.path.join(out_dir, signed_apk_name)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100417 as_utils.MoveFile(signed_apk, apk_dest)
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100418 else:
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100419 apk_dest = os.path.join(out_dir, unsigned_apk_name)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100420 as_utils.MoveFile(unsigned_apk, apk_dest)
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100421
422 assert IsBuiltWithR8(apk_dest) == ('r8' in shrinker), (
423 'Unexpected marker in generated APK for {}'.format(shrinker))
424
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100425 profile_dest_dir = os.path.join(out_dir, 'profile')
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100426 as_utils.MoveProfileReportTo(profile_dest_dir, stdout)
427
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100428 return (apk_dest, profile_dest_dir, proguard_config_dest)
429
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +0100430def RebuildAppWithShrinker(
431 apk, apk_dest, proguard_config_file, shrinker, min_sdk, compile_sdk):
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100432 assert 'r8' in shrinker
433 assert apk_dest.endswith('.apk')
Søren Gjesse9fb48802019-01-18 11:00:00 +0100434
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100435 # Compile given APK with shrinker to temporary zip file.
Christoffer Quist Adamsen17879c12019-01-22 16:13:54 +0100436 android_jar = utils.get_android_jar(compile_sdk)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100437 r8_jar = utils.R8LIB_JAR if IsMinifiedR8(shrinker) else utils.R8_JAR
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100438 zip_dest = apk_dest[:-4] + '.zip'
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100439
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +0100440 # TODO(christofferqa): Entry point should be CompatProguard if the shrinker
441 # is 'r8'.
442 entry_point = 'com.android.tools.r8.R8'
443
444 cmd = ['java', '-ea:com.android.tools.r8...', '-cp', r8_jar, entry_point,
445 '--release', '--min-api', str(min_sdk), '--pg-conf', proguard_config_file,
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100446 '--lib', android_jar, '--output', zip_dest, apk]
Christoffer Quist Adamsen17879c12019-01-22 16:13:54 +0100447
448 for android_optional_jar in utils.get_android_optional_jars(compile_sdk):
449 cmd.append('--lib')
450 cmd.append(android_optional_jar)
451
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100452 utils.PrintCmd(cmd)
453
454 subprocess.check_output(cmd)
455
456 # Make a copy of the given APK, move the newly generated dex files into the
457 # copied APK, and then sign the APK.
458 apk_masseur.masseur(apk, dex=zip_dest, out=apk_dest)
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100459
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100460def RunMonkey(app, config, options, apk_dest):
461 if not WaitForEmulator():
462 return False
463
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100464 UninstallApkOnEmulator(app, config)
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100465 InstallApkOnEmulator(apk_dest)
466
467 app_id = config.get('app_id')
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100468 number_of_events_to_generate = options.monkey_events
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100469
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100470 # Intentionally using a constant seed such that the monkey generates the same
471 # event sequence for each shrinker.
472 random_seed = 42
473
474 cmd = ['adb', 'shell', 'monkey', '-p', app_id, '-s', str(random_seed),
Christoffer Quist Adamsen88b7ebe2019-01-14 11:22:17 +0100475 str(number_of_events_to_generate)]
476 utils.PrintCmd(cmd)
477
478 try:
479 stdout = subprocess.check_output(cmd)
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100480 succeeded = (
481 'Events injected: {}'.format(number_of_events_to_generate) in stdout)
Christoffer Quist Adamsen88b7ebe2019-01-14 11:22:17 +0100482 except subprocess.CalledProcessError as e:
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100483 succeeded = False
Christoffer Quist Adamsen88b7ebe2019-01-14 11:22:17 +0100484
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100485 UninstallApkOnEmulator(app, config)
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100486
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100487 return succeeded
488
489def LogResultsForApps(result_per_shrinker_per_app, options):
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100490 for app, result_per_shrinker in result_per_shrinker_per_app.iteritems():
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100491 LogResultsForApp(app, result_per_shrinker, options)
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100492
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100493def LogResultsForApp(app, result_per_shrinker, options):
494 print(app + ':')
495
496 if result_per_shrinker.get('status') != 'success':
497 error_message = result_per_shrinker.get('error_message')
498 print(' skipped ({})'.format(error_message))
499 return
500
501 proguard_result = result_per_shrinker.get('proguard', {})
502 proguard_dex_size = float(proguard_result.get('dex_size', -1))
503 proguard_duration = sum(proguard_result.get('profile', {}).values())
504
505 for shrinker in SHRINKERS:
506 if shrinker not in result_per_shrinker:
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100507 continue
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100508 result = result_per_shrinker.get(shrinker)
509 build_status = result.get('build_status')
510 if build_status != 'success':
511 warn(' {}: {}'.format(shrinker, build_status))
512 else:
513 print(' {}:'.format(shrinker))
514 dex_size = result.get('dex_size')
515 msg = ' dex size: {}'.format(dex_size)
516 if dex_size != proguard_dex_size and proguard_dex_size >= 0:
517 msg = '{} ({}, {})'.format(
518 msg, dex_size - proguard_dex_size,
519 PercentageDiffAsString(proguard_dex_size, dex_size))
520 success(msg) if dex_size < proguard_dex_size else warn(msg)
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100521 else:
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100522 print(msg)
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100523
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100524 profile = result.get('profile')
525 duration = sum(profile.values())
526 msg = ' performance: {}s'.format(duration)
527 if duration != proguard_duration and proguard_duration > 0:
528 msg = '{} ({}s, {})'.format(
529 msg, duration - proguard_duration,
530 PercentageDiffAsString(proguard_duration, duration))
531 success(msg) if duration < proguard_duration else warn(msg)
532 else:
533 print(msg)
534 if len(profile) >= 2:
535 for task_name, task_duration in profile.iteritems():
536 print(' {}: {}s'.format(task_name, task_duration))
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100537
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100538 if options.monkey:
539 monkey_status = result.get('monkey_status')
540 if monkey_status != 'success':
541 warn(' monkey: {}'.format(monkey_status))
542 else:
543 success(' monkey: {}'.format(monkey_status))
544 recompilation_results = result.get('recompilation_results', [])
545 i = 0
546 for recompilation_result in recompilation_results:
547 build_status = recompilation_result.get('build_status')
548 if build_status != 'success':
549 print(' recompilation #{}: {}'.format(i, build_status))
550 else:
551 dex_size = recompilation_result.get('dex_size')
552 print(' recompilation #{}'.format(i))
553 print(' dex size: {}'.format(dex_size))
554 if options.monkey:
555 monkey_status = recompilation_result.get('monkey_status')
556 msg = ' monkey: {}'.format(monkey_status)
557 if monkey_status == 'success':
558 success(msg)
559 elif monkey_status == 'skipped':
560 print(msg)
561 else:
562 warn(msg)
563 i += 1
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100564
Søren Gjessecdae8792018-12-12 09:02:43 +0100565def ParseOptions(argv):
566 result = optparse.OptionParser()
567 result.add_option('--app',
568 help='What app to run on',
569 choices=APPS.keys())
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100570 result.add_option('--monkey',
571 help='Whether to install and run app(s) with monkey',
572 default=False,
573 action='store_true')
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100574 result.add_option('--monkey_events',
575 help='Number of events that the monkey should trigger',
576 default=250,
577 type=int)
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100578 result.add_option('--pull',
579 help='Whether to pull the latest version of each app',
580 default=False,
581 action='store_true')
Søren Gjesseeed839d2019-01-11 15:19:16 +0100582 result.add_option('--sign-apks', '--sign_apks',
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +0100583 help='Whether the APKs should be signed',
584 default=False,
585 action='store_true')
586 result.add_option('--shrinker',
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100587 help='The shrinkers to use (by default, all are run)',
588 action='append')
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100589 result.add_option('--r8-compilation-steps', '--r8_compilation_steps',
590 help='Number of times R8 should be run on each app',
591 default=2,
592 type=int)
Søren Gjesseeed839d2019-01-11 15:19:16 +0100593 result.add_option('--disable-tot', '--disable_tot',
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100594 help='Whether to disable the use of the ToT version of R8',
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100595 default=False,
596 action='store_true')
Søren Gjesseeed839d2019-01-11 15:19:16 +0100597 result.add_option('--no-build', '--no_build',
598 help='Run without building ToT first (only when using ToT)',
599 default=False,
600 action='store_true')
Søren Gjesse9fb48802019-01-18 11:00:00 +0100601 result.add_option('--gradle-flags', '--gradle_flags',
602 help='Flags to pass in to gradle')
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100603 (options, args) = result.parse_args(argv)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100604 if options.disable_tot:
605 # r8.jar is required for recompiling the generated APK
606 options.r8_compilation_steps = 1
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100607 if options.shrinker:
608 for shrinker in options.shrinker:
609 assert shrinker in SHRINKERS
610 return (options, args)
Søren Gjessecdae8792018-12-12 09:02:43 +0100611
612def main(argv):
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100613 global SHRINKERS
614
Søren Gjessecdae8792018-12-12 09:02:43 +0100615 (options, args) = ParseOptions(argv)
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100616 assert options.disable_tot or os.path.isfile(utils.R8_JAR), (
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +0100617 'Cannot build from ToT without r8.jar')
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100618 assert options.disable_tot or os.path.isfile(utils.R8LIB_JAR), (
619 'Cannot build from ToT without r8lib.jar')
620
621 if options.disable_tot:
622 # Cannot run r8 lib without adding r8lib.jar as an dependency
623 SHRINKERS = [
624 shrinker for shrinker in SHRINKERS
625 if 'minified' not in shrinker]
Søren Gjessecdae8792018-12-12 09:02:43 +0100626
Søren Gjesseeed839d2019-01-11 15:19:16 +0100627 if not options.no_build and not options.disable_tot:
628 gradle.RunGradle(['r8', 'r8lib'])
629
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100630 result_per_shrinker_per_app = {}
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100631
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100632 if options.app:
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100633 result_per_shrinker_per_app[options.app] = GetResultsForApp(
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100634 options.app, APPS.get(options.app), options)
Søren Gjessecdae8792018-12-12 09:02:43 +0100635 else:
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100636 for app, config in APPS.iteritems():
637 if not config.get('skip', False):
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100638 result_per_shrinker_per_app[app] = GetResultsForApp(
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100639 app, config, options)
640
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100641 LogResultsForApps(result_per_shrinker_per_app, options)
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100642
643def success(message):
644 CGREEN = '\033[32m'
645 CEND = '\033[0m'
646 print(CGREEN + message + CEND)
647
648def warn(message):
649 CRED = '\033[91m'
650 CEND = '\033[0m'
651 print(CRED + message + CEND)
Søren Gjessecdae8792018-12-12 09:02:43 +0100652
653if __name__ == '__main__':
654 sys.exit(main(sys.argv[1:]))