blob: 1d8b04a98caa7003763bac6711c62dc60112374b [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
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010011import shutil
Søren Gjessecdae8792018-12-12 09:02:43 +010012import subprocess
13import sys
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',
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010039 'min_sdk': 17
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010040 },
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010041 'AntennaPod': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010042 'app_id': 'de.danoeh.antennapod',
Christoffer Quist Adamsen4e661df2019-01-22 10:03:06 +010043 'git_repo': 'https://github.com/christofferqa/AntennaPod.git',
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010044 'flavor': 'play',
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +010045 'min_sdk': 14,
46 'compile_sdk': 26
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010047 },
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010048 'apps-android-wikipedia': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010049 'app_id': 'org.wikipedia',
Christoffer Quist Adamsen4e661df2019-01-22 10:03:06 +010050 'git_repo': 'https://github.com/christofferqa/apps-android-wikipedia',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010051 'flavor': 'prod',
52 'signed-apk-name': 'app-prod-universal-release.apk'
53 },
Morten Krogh-Jespersen2dbbc1b2019-01-24 16:31:52 +010054 'chanu': {
55 'app_id': 'com.chanapps.four.activity',
56 'git_repo': 'https://github.com/mkj-gram/chanu.git',
57 },
Christoffer Quist Adamsen61b8c802018-12-20 08:18:40 +010058 'friendlyeats-android': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010059 'app_id': 'com.google.firebase.example.fireeats',
Christoffer Quist Adamsen4e661df2019-01-22 10:03:06 +010060 'git_repo': 'https://github.com/christofferqa/friendlyeats-android.git'
Christoffer Quist Adamsen61b8c802018-12-20 08:18:40 +010061 },
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010062 'KISS': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010063 'app_id': 'fr.neamar.kiss',
Christoffer Quist Adamsen4e661df2019-01-22 10:03:06 +010064 'git_repo': 'https://github.com/christofferqa/KISS',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010065 },
66 'materialistic': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010067 'app_id': 'io.github.hidroh.materialistic',
Christoffer Quist Adamsen4e661df2019-01-22 10:03:06 +010068 'git_repo': 'https://github.com/christofferqa/materialistic',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010069 },
70 'Minimal-Todo': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010071 'app_id': 'com.avjindersinghsekhon.minimaltodo',
Christoffer Quist Adamsen4e661df2019-01-22 10:03:06 +010072 'git_repo': 'https://github.com/christofferqa/Minimal-Todo',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010073 },
74 'NewPipe': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010075 'app_id': 'org.schabi.newpipe',
Christoffer Quist Adamsen4e661df2019-01-22 10:03:06 +010076 'git_repo': 'https://github.com/christofferqa/NewPipe',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010077 },
Morten Krogh-Jespersen4293dcd2019-01-28 11:38:50 +010078 'rover-android': {
79 'app_id': 'io.rover.app.debug',
80 'app_module': 'debug-app',
81 'git_repo': 'https://github.com/mkj-gram/rover-android.git',
82 },
Morten Krogh-Jespersen2dbbc1b2019-01-24 16:31:52 +010083 'Signal-Android': {
84 'app_id': 'org.thoughtcrime.securesms',
85 'app_module': '',
86 'flavor': 'play',
87 'git_repo': 'https://github.com/mkj-gram/Signal-Android.git',
88 'releaseTarget': 'assemblePlayRelease',
89 'signed-apk-name': 'Signal-play-release-4.32.7.apk',
90 },
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010091 'Simple-Calendar': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010092 'app_id': 'com.simplemobiletools.calendar.pro',
Christoffer Quist Adamsen4e661df2019-01-22 10:03:06 +010093 'git_repo': 'https://github.com/christofferqa/Simple-Calendar',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +010094 'signed-apk-name': 'calendar-release.apk'
95 },
Søren Gjessecdae8792018-12-12 09:02:43 +010096 'tachiyomi': {
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +010097 'app_id': 'eu.kanade.tachiyomi',
Søren Gjessecdae8792018-12-12 09:02:43 +010098 'git_repo': 'https://github.com/sgjesse/tachiyomi.git',
99 'flavor': 'standard',
Søren Gjesse8c111482018-12-21 14:47:57 +0100100 'releaseTarget': 'app:assembleRelease',
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +0100101 'min_sdk': 16
Søren Gjessecdae8792018-12-12 09:02:43 +0100102 },
Søren Gjesse9fb48802019-01-18 11:00:00 +0100103 'tivi': {
104 'app_id': 'app.tivi',
Søren Gjesse9fb48802019-01-18 11:00:00 +0100105 'git_repo': 'https://github.com/sgjesse/tivi.git',
Christoffer Quist Adamsen3b6f1062019-02-07 09:49:43 +0100106 'min_sdk': 23,
107 'compile_sdk': 28,
Søren Gjesse9fb48802019-01-18 11:00:00 +0100108 },
Morten Krogh-Jespersen580dc642019-01-25 10:18:22 +0100109 'Tusky': {
110 'app_id': 'com.keylesspalace.tusky',
111 'git_repo': 'https://github.com/mkj-gram/Tusky.git',
112 'flavor': 'blue'
113 },
Morten Krogh-Jespersenaf6377d2019-01-29 11:18:31 +0100114 'Vungle-Android-SDK': {
115 'app_id': 'com.publisher.vungle.sample',
116 'git_repo': 'https://github.com/mkj-gram/Vungle-Android-SDK.git',
117 },
Søren Gjessecdae8792018-12-12 09:02:43 +0100118 # This does not build yet.
119 'muzei': {
120 'git_repo': 'https://github.com/sgjesse/muzei.git',
121 'app_module': 'main',
122 'archives_base_name': 'muzei',
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100123 'skip': True,
Søren Gjessecdae8792018-12-12 09:02:43 +0100124 },
125}
126
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100127# TODO(christofferqa): Do not rely on 'emulator-5554' name
128emulator_id = 'emulator-5554'
129
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100130def ComputeSizeOfDexFilesInApk(apk):
131 dex_size = 0
132 z = zipfile.ZipFile(apk, 'r')
133 for filename in z.namelist():
134 if filename.endswith('.dex'):
135 dex_size += z.getinfo(filename).file_size
136 return dex_size
137
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100138def IsBuiltWithR8(apk, temp_dir, options):
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100139 r8_jar = os.path.join(temp_dir, 'r8.jar')
140
141 # Use the copy of r8.jar if it is there.
142 if os.path.isfile(r8_jar):
143 cmd = ['java', '-ea', '-jar', r8_jar, 'extractmarker', apk]
144 else:
145 script = os.path.join(utils.TOOLS_DIR, 'extractmarker.py')
146 cmd = ['python', script, apk]
147
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100148 utils.PrintCmd(cmd, quiet=options.quiet)
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100149 return '~~R8' in subprocess.check_output(cmd).strip()
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +0100150
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100151def IsMinifiedR8(shrinker):
152 return shrinker == 'r8-minified' or shrinker == 'r8full-minified'
153
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +0100154def IsTrackedByGit(file):
155 return subprocess.check_output(['git', 'ls-files', file]).strip() != ''
156
Søren Gjessecdae8792018-12-12 09:02:43 +0100157def GitClone(git_url):
158 return subprocess.check_output(['git', 'clone', git_url]).strip()
159
160def GitPull():
Christoffer Quist Adamsen01c7f0b2019-01-10 10:59:16 +0100161 # Use --no-edit to accept the auto-generated merge message, if any.
162 return subprocess.call(['git', 'pull', '--no-edit']) == 0
Søren Gjessecdae8792018-12-12 09:02:43 +0100163
164def GitCheckout(file):
165 return subprocess.check_output(['git', 'checkout', file]).strip()
166
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100167def InstallApkOnEmulator(apk_dest, options):
168 cmd = ['adb', '-s', emulator_id, 'install', '-r', '-d', apk_dest]
169 if options.quiet:
170 with open(os.devnull, 'w') as devnull:
171 subprocess.check_call(cmd, stdout=devnull)
172 else:
173 subprocess.check_call(cmd)
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100174
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100175def PercentageDiffAsString(before, after):
176 if after < before:
177 return '-' + str(round((1.0 - after / before) * 100)) + '%'
178 else:
179 return '+' + str(round((after - before) / before * 100)) + '%'
180
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100181def UninstallApkOnEmulator(app, config, options):
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100182 app_id = config.get('app_id')
183 process = subprocess.Popen(
184 ['adb', 'uninstall', app_id],
185 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
186 stdout, stderr = process.communicate()
187
188 if stdout.strip() == 'Success':
189 # Successfully uninstalled
190 return
191
192 if 'Unknown package: {}'.format(app_id) in stderr:
193 # Application not installed
194 return
195
196 raise Exception(
197 'Unexpected result from `adb uninstall {}\nStdout: {}\nStderr: {}'.format(
198 app_id, stdout, stderr))
199
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100200def WaitForEmulator():
201 stdout = subprocess.check_output(['adb', 'devices'])
202 if '{}\tdevice'.format(emulator_id) in stdout:
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100203 return True
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100204
205 print('Emulator \'{}\' not connected; waiting for connection'.format(
206 emulator_id))
207
208 time_waited = 0
209 while True:
210 time.sleep(10)
211 time_waited += 10
212 stdout = subprocess.check_output(['adb', 'devices'])
213 if '{}\tdevice'.format(emulator_id) not in stdout:
214 print('... still waiting for connection')
215 if time_waited >= 5 * 60:
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100216 return False
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100217 else:
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100218 return True
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100219
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100220def GetResultsForApp(app, config, options, temp_dir):
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100221 git_repo = config['git_repo']
222
223 # Checkout and build in the build directory.
224 checkout_dir = os.path.join(WORKING_DIR, app)
225
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100226 result = {}
227
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100228 if not os.path.exists(checkout_dir):
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100229 with utils.ChangedWorkingDirectory(WORKING_DIR, quiet=options.quiet):
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100230 GitClone(git_repo)
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100231 elif options.pull:
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100232 with utils.ChangedWorkingDirectory(checkout_dir, quiet=options.quiet):
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100233 # Checkout build.gradle to avoid merge conflicts.
234 if IsTrackedByGit('build.gradle'):
235 GitCheckout('build.gradle')
236
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100237 if not GitPull():
238 result['status'] = 'failed'
239 result['error_message'] = 'Unable to pull from remote'
240 return result
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100241
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100242 result['status'] = 'success'
243
244 result_per_shrinker = BuildAppWithSelectedShrinkers(
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100245 app, config, options, checkout_dir, temp_dir)
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100246 for shrinker, shrinker_result in result_per_shrinker.iteritems():
247 result[shrinker] = shrinker_result
248
249 return result
250
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100251def BuildAppWithSelectedShrinkers(app, config, options, checkout_dir, temp_dir):
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100252 result_per_shrinker = {}
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100253
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100254 with utils.ChangedWorkingDirectory(checkout_dir, quiet=options.quiet):
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100255 for shrinker in SHRINKERS:
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100256 if options.shrinker and shrinker not in options.shrinker:
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100257 continue
258
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100259 apk_dest = None
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100260
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100261 result = {}
262 try:
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100263 out_dir = os.path.join(checkout_dir, 'out', shrinker)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100264 (apk_dest, profile_dest_dir, proguard_config_file) = \
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100265 BuildAppWithShrinker(app, config, shrinker, checkout_dir, out_dir,
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100266 temp_dir, options)
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100267 dex_size = ComputeSizeOfDexFilesInApk(apk_dest)
268 result['apk_dest'] = apk_dest,
269 result['build_status'] = 'success'
270 result['dex_size'] = dex_size
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100271 result['profile_dest_dir'] = profile_dest_dir
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100272
273 profile = as_utils.ParseProfileReport(profile_dest_dir)
274 result['profile'] = {
275 task_name:duration for task_name, duration in profile.iteritems()
276 if as_utils.IsGradleCompilerTask(task_name, shrinker)}
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100277 except Exception as e:
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100278 warn('Failed to build {} with {}'.format(app, shrinker))
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100279 if e:
280 print('Error: ' + str(e))
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100281 result['build_status'] = 'failed'
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100282
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100283 if result.get('build_status') == 'success':
284 if options.monkey:
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100285 result['monkey_status'] = 'success' if RunMonkey(
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100286 app, config, options, apk_dest) else 'failed'
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100287
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100288 if 'r8' in shrinker and options.r8_compilation_steps > 1:
289 recompilation_results = []
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100290
291 # Build app with gradle using -D...keepRuleSynthesisForRecompilation=
292 # true.
293 out_dir = os.path.join(checkout_dir, 'out', shrinker + '-1')
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100294 (apk_dest, profile_dest_dir, ext_proguard_config_file) = \
295 BuildAppWithShrinker(app, config, shrinker, checkout_dir, out_dir,
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100296 temp_dir, options, keepRuleSynthesisForRecompilation=True)
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100297 dex_size = ComputeSizeOfDexFilesInApk(apk_dest)
298 recompilation_result = {
299 'apk_dest': apk_dest,
300 'build_status': 'success',
301 'dex_size': ComputeSizeOfDexFilesInApk(apk_dest),
302 'monkey_status': 'skipped'
303 }
304 recompilation_results.append(recompilation_result)
305
306 # Sanity check that keep rules have changed.
307 with open(ext_proguard_config_file) as new:
308 with open(proguard_config_file) as old:
Christoffer Quist Adamsend2bfcde2019-01-22 10:05:15 +0100309 assert(sum(1 for line in new) > sum(1 for line in old))
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100310
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +0100311 # Extract min-sdk and target-sdk
312 (min_sdk, compile_sdk) = as_utils.GetMinAndCompileSdk(app, config,
313 checkout_dir, apk_dest)
314
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100315 # Now rebuild generated apk.
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100316 previous_apk = apk_dest
317 for i in range(1, options.r8_compilation_steps):
318 try:
319 recompiled_apk_dest = os.path.join(
320 checkout_dir, 'out', shrinker, 'app-release-{}.apk'.format(i))
321 RebuildAppWithShrinker(
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100322 app, previous_apk, recompiled_apk_dest,
323 ext_proguard_config_file, shrinker, min_sdk, compile_sdk,
324 options, temp_dir)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100325 recompilation_result = {
326 'apk_dest': recompiled_apk_dest,
327 'build_status': 'success',
328 'dex_size': ComputeSizeOfDexFilesInApk(recompiled_apk_dest)
329 }
330 if options.monkey:
331 recompilation_result['monkey_status'] = 'success' if RunMonkey(
332 app, config, options, recompiled_apk_dest) else 'failed'
333 recompilation_results.append(recompilation_result)
334 previous_apk = recompiled_apk_dest
335 except Exception as e:
336 warn('Failed to recompile {} with {}'.format(app, shrinker))
337 recompilation_results.append({ 'build_status': 'failed' })
338 break
339 result['recompilation_results'] = recompilation_results
340
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100341 result_per_shrinker[shrinker] = result
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100342
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100343 if not options.app:
344 print('')
345 LogResultsForApp(app, result_per_shrinker, options)
346 print('')
347
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100348 return result_per_shrinker
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100349
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100350def BuildAppWithShrinker(
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100351 app, config, shrinker, checkout_dir, out_dir, temp_dir, options,
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100352 keepRuleSynthesisForRecompilation=False):
353 print('Building {} with {}{}'.format(
354 app,
355 shrinker,
356 ' for recompilation' if keepRuleSynthesisForRecompilation else ''))
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100357
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100358 # Add/remove 'r8.jar' from top-level build.gradle.
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100359 if options.disable_tot:
360 as_utils.remove_r8_dependency(checkout_dir)
361 else:
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100362 as_utils.add_r8_dependency(checkout_dir, temp_dir, IsMinifiedR8(shrinker))
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100363
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100364 app_module = config.get('app_module', 'app')
Morten Krogh-Jespersen47d62ba2019-01-22 08:35:49 +0100365 archives_base_name = config.get('archives_base_name', app_module)
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100366 flavor = config.get('flavor')
367
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100368 if not os.path.exists(out_dir):
369 os.makedirs(out_dir)
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100370
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100371 # Set -printconfiguration in Proguard rules.
372 proguard_config_dest = os.path.abspath(
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100373 os.path.join(out_dir, 'proguard-rules.pro'))
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100374 as_utils.SetPrintConfigurationDirective(
375 app, config, checkout_dir, proguard_config_dest)
376
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100377 env = {}
Morten Krogh-Jespersenc8efedd2019-01-28 11:36:17 +0100378 env['ANDROID_HOME'] = utils.ANDROID_HOME
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100379 env['JAVA_OPTS'] = '-ea:com.android.tools.r8...'
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100380
Søren Gjesse8c111482018-12-21 14:47:57 +0100381 releaseTarget = config.get('releaseTarget')
382 if not releaseTarget:
383 releaseTarget = app_module + ':' + 'assemble' + (
384 flavor.capitalize() if flavor else '') + 'Release'
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100385
Søren Gjesse9fb48802019-01-18 11:00:00 +0100386 # Value for property android.enableR8.
387 enableR8 = 'r8' in shrinker
388 # Value for property android.enableR8.fullMode.
389 enableR8FullMode = shrinker == 'r8full' or shrinker == 'r8full-minified'
390 # Build gradlew command line.
391 cmd = ['./gradlew', '--no-daemon', 'clean', releaseTarget,
392 '--profile', '--stacktrace',
393 '-Pandroid.enableR8=' + str(enableR8).lower(),
394 '-Pandroid.enableR8.fullMode=' + str(enableR8FullMode).lower()]
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100395 if keepRuleSynthesisForRecompilation:
396 cmd.append('-Dcom.android.tools.r8.keepRuleSynthesisForRecompilation=true')
Søren Gjesse9fb48802019-01-18 11:00:00 +0100397 if options.gradle_flags:
398 cmd.extend(options.gradle_flags.split(' '))
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100399
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100400 stdout = utils.RunCmd(cmd, env, quiet=options.quiet)
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100401
402 apk_base_name = (archives_base_name
403 + (('-' + flavor) if flavor else '') + '-release')
404 signed_apk_name = config.get('signed-apk-name', apk_base_name + '.apk')
405 unsigned_apk_name = apk_base_name + '-unsigned.apk'
406
407 build_dir = config.get('build_dir', 'build')
408 build_output_apks = os.path.join(app_module, build_dir, 'outputs', 'apk')
409 if flavor:
410 build_output_apks = os.path.join(build_output_apks, flavor, 'release')
411 else:
412 build_output_apks = os.path.join(build_output_apks, 'release')
413
414 signed_apk = os.path.join(build_output_apks, signed_apk_name)
415 unsigned_apk = os.path.join(build_output_apks, unsigned_apk_name)
416
417 if options.sign_apks and not os.path.isfile(signed_apk):
418 assert os.path.isfile(unsigned_apk)
419 if options.sign_apks:
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100420 apk_utils.sign_with_apksigner(
Morten Krogh-Jespersenc8efedd2019-01-28 11:36:17 +0100421 utils.ANDROID_BUILD_TOOLS,
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100422 unsigned_apk,
423 signed_apk,
Christoffer Quist Adamsen3b6f1062019-02-07 09:49:43 +0100424 options.keystore,
425 options.keystore_password,
426 quiet=options.quiet)
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100427
428 if os.path.isfile(signed_apk):
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100429 apk_dest = os.path.join(out_dir, signed_apk_name)
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100430 as_utils.MoveFile(signed_apk, apk_dest, quiet=options.quiet)
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100431 else:
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100432 apk_dest = os.path.join(out_dir, unsigned_apk_name)
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100433 as_utils.MoveFile(unsigned_apk, apk_dest, quiet=options.quiet)
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100434
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100435 assert IsBuiltWithR8(apk_dest, temp_dir, options) == ('r8' in shrinker), (
Christoffer Quist Adamsen3aa8d252018-12-13 14:56:40 +0100436 'Unexpected marker in generated APK for {}'.format(shrinker))
437
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100438 profile_dest_dir = os.path.join(out_dir, 'profile')
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100439 as_utils.MoveProfileReportTo(profile_dest_dir, stdout, quiet=options.quiet)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100440
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100441 return (apk_dest, profile_dest_dir, proguard_config_dest)
442
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +0100443def RebuildAppWithShrinker(
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100444 app, apk, apk_dest, proguard_config_file, shrinker, min_sdk, compile_sdk,
445 options, temp_dir):
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100446 assert 'r8' in shrinker
447 assert apk_dest.endswith('.apk')
Søren Gjesse9fb48802019-01-18 11:00:00 +0100448
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100449 print('Rebuilding {} with {}'.format(app, shrinker))
450
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100451 # Compile given APK with shrinker to temporary zip file.
Christoffer Quist Adamsen17879c12019-01-22 16:13:54 +0100452 android_jar = utils.get_android_jar(compile_sdk)
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100453 r8_jar = os.path.join(
454 temp_dir, 'r8lib.jar' if IsMinifiedR8(shrinker) else 'r8.jar')
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100455 zip_dest = apk_dest[:-4] + '.zip'
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100456
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +0100457 # TODO(christofferqa): Entry point should be CompatProguard if the shrinker
458 # is 'r8'.
459 entry_point = 'com.android.tools.r8.R8'
460
461 cmd = ['java', '-ea:com.android.tools.r8...', '-cp', r8_jar, entry_point,
462 '--release', '--min-api', str(min_sdk), '--pg-conf', proguard_config_file,
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100463 '--lib', android_jar, '--output', zip_dest, apk]
Christoffer Quist Adamsen17879c12019-01-22 16:13:54 +0100464
465 for android_optional_jar in utils.get_android_optional_jars(compile_sdk):
466 cmd.append('--lib')
467 cmd.append(android_optional_jar)
468
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100469 utils.RunCmd(cmd, quiet=options.quiet)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100470
471 # Make a copy of the given APK, move the newly generated dex files into the
472 # copied APK, and then sign the APK.
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100473 apk_masseur.masseur(
474 apk, dex=zip_dest, out=apk_dest, quiet=options.quiet)
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100475
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100476def RunMonkey(app, config, options, apk_dest):
477 if not WaitForEmulator():
478 return False
479
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100480 UninstallApkOnEmulator(app, config, options)
481 InstallApkOnEmulator(apk_dest, options)
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100482
483 app_id = config.get('app_id')
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100484 number_of_events_to_generate = options.monkey_events
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100485
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100486 # Intentionally using a constant seed such that the monkey generates the same
487 # event sequence for each shrinker.
488 random_seed = 42
489
490 cmd = ['adb', 'shell', 'monkey', '-p', app_id, '-s', str(random_seed),
Christoffer Quist Adamsen88b7ebe2019-01-14 11:22:17 +0100491 str(number_of_events_to_generate)]
Christoffer Quist Adamsen88b7ebe2019-01-14 11:22:17 +0100492
493 try:
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100494 stdout = utils.RunCmd(cmd, quiet=options.quiet)
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100495 succeeded = (
496 'Events injected: {}'.format(number_of_events_to_generate) in stdout)
Christoffer Quist Adamsen88b7ebe2019-01-14 11:22:17 +0100497 except subprocess.CalledProcessError as e:
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100498 succeeded = False
Christoffer Quist Adamsen88b7ebe2019-01-14 11:22:17 +0100499
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100500 UninstallApkOnEmulator(app, config, options)
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100501
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100502 return succeeded
503
504def LogResultsForApps(result_per_shrinker_per_app, options):
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100505 print('')
506 for app, result_per_shrinker in sorted(
507 result_per_shrinker_per_app.iteritems()):
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100508 LogResultsForApp(app, result_per_shrinker, options)
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100509
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100510def LogResultsForApp(app, result_per_shrinker, options):
511 print(app + ':')
512
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100513 if result_per_shrinker.get('status', 'success') != 'success':
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100514 error_message = result_per_shrinker.get('error_message')
515 print(' skipped ({})'.format(error_message))
516 return
517
518 proguard_result = result_per_shrinker.get('proguard', {})
519 proguard_dex_size = float(proguard_result.get('dex_size', -1))
520 proguard_duration = sum(proguard_result.get('profile', {}).values())
521
522 for shrinker in SHRINKERS:
523 if shrinker not in result_per_shrinker:
Christoffer Quist Adamsen724bfb02019-01-10 09:54:38 +0100524 continue
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100525 result = result_per_shrinker.get(shrinker)
526 build_status = result.get('build_status')
527 if build_status != 'success':
528 warn(' {}: {}'.format(shrinker, build_status))
529 else:
530 print(' {}:'.format(shrinker))
531 dex_size = result.get('dex_size')
532 msg = ' dex size: {}'.format(dex_size)
533 if dex_size != proguard_dex_size and proguard_dex_size >= 0:
534 msg = '{} ({}, {})'.format(
535 msg, dex_size - proguard_dex_size,
536 PercentageDiffAsString(proguard_dex_size, dex_size))
537 success(msg) if dex_size < proguard_dex_size else warn(msg)
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100538 else:
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100539 print(msg)
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100540
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100541 profile = result.get('profile')
542 duration = sum(profile.values())
543 msg = ' performance: {}s'.format(duration)
544 if duration != proguard_duration and proguard_duration > 0:
545 msg = '{} ({}s, {})'.format(
546 msg, duration - proguard_duration,
547 PercentageDiffAsString(proguard_duration, duration))
548 success(msg) if duration < proguard_duration else warn(msg)
549 else:
550 print(msg)
551 if len(profile) >= 2:
552 for task_name, task_duration in profile.iteritems():
553 print(' {}: {}s'.format(task_name, task_duration))
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100554
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100555 if options.monkey:
556 monkey_status = result.get('monkey_status')
557 if monkey_status != 'success':
558 warn(' monkey: {}'.format(monkey_status))
559 else:
560 success(' monkey: {}'.format(monkey_status))
561 recompilation_results = result.get('recompilation_results', [])
562 i = 0
563 for recompilation_result in recompilation_results:
564 build_status = recompilation_result.get('build_status')
565 if build_status != 'success':
566 print(' recompilation #{}: {}'.format(i, build_status))
567 else:
568 dex_size = recompilation_result.get('dex_size')
569 print(' recompilation #{}'.format(i))
570 print(' dex size: {}'.format(dex_size))
571 if options.monkey:
572 monkey_status = recompilation_result.get('monkey_status')
573 msg = ' monkey: {}'.format(monkey_status)
574 if monkey_status == 'success':
575 success(msg)
576 elif monkey_status == 'skipped':
577 print(msg)
578 else:
579 warn(msg)
580 i += 1
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100581
Søren Gjessecdae8792018-12-12 09:02:43 +0100582def ParseOptions(argv):
583 result = optparse.OptionParser()
584 result.add_option('--app',
585 help='What app to run on',
586 choices=APPS.keys())
Christoffer Quist Adamsen3b6f1062019-02-07 09:49:43 +0100587 result.add_option('--keystore',
588 help='Path to app.keystore',
589 default='app.keystore')
590 result.add_option('--keystore-password', '--keystore_password',
591 help='Password for app.keystore',
592 default='android')
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100593 result.add_option('--monkey',
594 help='Whether to install and run app(s) with monkey',
595 default=False,
596 action='store_true')
Christoffer Quist Adamsenbae36612019-01-15 12:23:51 +0100597 result.add_option('--monkey_events',
598 help='Number of events that the monkey should trigger',
599 default=250,
600 type=int)
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100601 result.add_option('--pull',
602 help='Whether to pull the latest version of each app',
603 default=False,
604 action='store_true')
Søren Gjesseeed839d2019-01-11 15:19:16 +0100605 result.add_option('--sign-apks', '--sign_apks',
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +0100606 help='Whether the APKs should be signed',
607 default=False,
608 action='store_true')
609 result.add_option('--shrinker',
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100610 help='The shrinkers to use (by default, all are run)',
611 action='append')
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100612 result.add_option('--r8-compilation-steps', '--r8_compilation_steps',
613 help='Number of times R8 should be run on each app',
614 default=2,
615 type=int)
Søren Gjesseeed839d2019-01-11 15:19:16 +0100616 result.add_option('--disable-tot', '--disable_tot',
Christoffer Quist Adamsen1d0a0fe2018-12-21 14:28:56 +0100617 help='Whether to disable the use of the ToT version of R8',
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100618 default=False,
619 action='store_true')
Søren Gjesseeed839d2019-01-11 15:19:16 +0100620 result.add_option('--no-build', '--no_build',
621 help='Run without building ToT first (only when using ToT)',
622 default=False,
623 action='store_true')
Søren Gjesse9fb48802019-01-18 11:00:00 +0100624 result.add_option('--gradle-flags', '--gradle_flags',
625 help='Flags to pass in to gradle')
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100626 result.add_option('--quiet',
627 help='Disable verbose logging',
628 default=False,
629 action='store_true')
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100630 (options, args) = result.parse_args(argv)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100631 if options.disable_tot:
632 # r8.jar is required for recompiling the generated APK
633 options.r8_compilation_steps = 1
Christoffer Quist Adamsen860fa932019-01-10 14:27:39 +0100634 if options.shrinker:
635 for shrinker in options.shrinker:
636 assert shrinker in SHRINKERS
637 return (options, args)
Søren Gjessecdae8792018-12-12 09:02:43 +0100638
639def main(argv):
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100640 global SHRINKERS
641
Søren Gjessecdae8792018-12-12 09:02:43 +0100642 (options, args) = ParseOptions(argv)
Christoffer Quist Adamsenf8ad4792019-01-09 13:19:19 +0100643
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100644 with utils.TempDir() as temp_dir:
645 if options.disable_tot:
646 # Cannot run r8 lib without adding r8lib.jar as an dependency
647 SHRINKERS = [
648 shrinker for shrinker in SHRINKERS
649 if 'minified' not in shrinker]
650 else:
651 if not options.no_build:
652 gradle.RunGradle(['r8', 'r8lib'])
Søren Gjessecdae8792018-12-12 09:02:43 +0100653
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100654 assert os.path.isfile(utils.R8_JAR), (
655 'Cannot build from ToT without r8.jar')
656 assert os.path.isfile(utils.R8LIB_JAR), (
657 'Cannot build from ToT without r8lib.jar')
Søren Gjesseeed839d2019-01-11 15:19:16 +0100658
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100659 # Make a copy of r8.jar and r8lib.jar such that they stay the same for
660 # the entire execution of this script.
661 shutil.copyfile(utils.R8_JAR, os.path.join(temp_dir, 'r8.jar'))
662 shutil.copyfile(utils.R8LIB_JAR, os.path.join(temp_dir, 'r8lib.jar'))
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100663
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100664 result_per_shrinker_per_app = {}
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100665
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100666 if options.app:
667 result_per_shrinker_per_app[options.app] = GetResultsForApp(
668 options.app, APPS.get(options.app), options, temp_dir)
669 else:
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100670 for app, config in sorted(APPS.iteritems()):
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +0100671 if not config.get('skip', False):
672 result_per_shrinker_per_app[app] = GetResultsForApp(
673 app, config, options, temp_dir)
674
675 LogResultsForApps(result_per_shrinker_per_app, options)
Christoffer Quist Adamsen404aade2018-12-20 13:00:09 +0100676
677def success(message):
678 CGREEN = '\033[32m'
679 CEND = '\033[0m'
680 print(CGREEN + message + CEND)
681
682def warn(message):
683 CRED = '\033[91m'
684 CEND = '\033[0m'
685 print(CRED + message + CEND)
Søren Gjessecdae8792018-12-12 09:02:43 +0100686
687if __name__ == '__main__':
688 sys.exit(main(sys.argv[1:]))