| #!/usr/bin/env python3 | 
 | # Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file | 
 | # for details. All rights reserved. Use of this source code is governed by a | 
 | # BSD-style license that can be found in the LICENSE file. | 
 |  | 
 | # Script for building sample apks using the sdk tools directly. | 
 |  | 
 | import apk_utils | 
 | import fnmatch | 
 | import glob | 
 | import jdk | 
 | import optparse | 
 | import os | 
 | import shutil | 
 | import subprocess | 
 | import sys | 
 | import time | 
 | import utils | 
 | import uuid | 
 |  | 
 | DEFAULT_AAPT = 'aapt'  # Assume in path. | 
 | DEFAULT_AAPT2 = 'aapt2'  # Assume in path. | 
 | DEFAULT_D8 = os.path.join(utils.REPO_ROOT, 'tools', 'd8.py') | 
 | DEFAULT_DEXSPLITTER = os.path.join(utils.REPO_ROOT, 'tools', 'dexsplitter.py') | 
 | DEFAULT_JAVAC = jdk.GetJavacExecutable() | 
 | SRC_LOCATION = 'src/com/android/tools/r8/sample/{app}/*.java' | 
 | DEFAULT_KEYSTORE = os.path.join(os.getenv('HOME'), '.android', 'debug.keystore') | 
 | PACKAGE_PREFIX = 'com.android.tools.r8.sample' | 
 | STANDARD_ACTIVITY = "R8Activity" | 
 | BENCHMARK_ITERATIONS = 30 | 
 |  | 
 | SAMPLE_APKS = ['simple', 'split'] | 
 |  | 
 |  | 
 | def parse_options(): | 
 |     result = optparse.OptionParser() | 
 |     result.add_option('--aapt', | 
 |                       help='aapt executable to use', | 
 |                       default=DEFAULT_AAPT) | 
 |     result.add_option('--aapt2', | 
 |                       help='aapt2 executable to use', | 
 |                       default=DEFAULT_AAPT2) | 
 |     result.add_option( | 
 |         '--api', | 
 |         help='Android api level', | 
 |         default=21, | 
 |         choices=['14', '15', '19', '21', '22', '23', '24', '25', '26']) | 
 |     result.add_option('--keystore', | 
 |                       help='Keystore used for signing', | 
 |                       default=DEFAULT_KEYSTORE) | 
 |     result.add_option('--split', | 
 |                       help='Split the app using the split.spec file', | 
 |                       default=False, | 
 |                       action='store_true') | 
 |     result.add_option( | 
 |         '--generate-proto-apk', | 
 |         help='Use aapt2 to generate the proto version of the apk.', | 
 |         default=False, | 
 |         action='store_true') | 
 |     result.add_option('--install', | 
 |                       help='Install the app (including featuresplit)', | 
 |                       default=False, | 
 |                       action='store_true') | 
 |     result.add_option( | 
 |         '--benchmark', | 
 |         help='Benchmark the app on the phone with specialized markers', | 
 |         default=False, | 
 |         action='store_true') | 
 |     result.add_option('--benchmark-output-dir', | 
 |                       help='Store benchmark results here.', | 
 |                       default=None) | 
 |     result.add_option('--app', | 
 |                       help='Which app to build', | 
 |                       default='simple', | 
 |                       choices=SAMPLE_APKS) | 
 |     return result.parse_args() | 
 |  | 
 |  | 
 | def run_aapt(aapt, args): | 
 |     command = [aapt] | 
 |     command.extend(args) | 
 |     utils.PrintCmd(command) | 
 |     subprocess.check_call(command) | 
 |  | 
 |  | 
 | def get_build_dir(app): | 
 |     return os.path.join(utils.BUILD, 'sampleApks', app) | 
 |  | 
 |  | 
 | def get_gen_path(app): | 
 |     gen_path = os.path.join(get_build_dir(app), 'gen') | 
 |     utils.makedirs_if_needed(gen_path) | 
 |     return gen_path | 
 |  | 
 |  | 
 | def get_bin_path(app): | 
 |     bin_path = os.path.join(get_build_dir(app), 'bin') | 
 |     utils.makedirs_if_needed(bin_path) | 
 |     return bin_path | 
 |  | 
 |  | 
 | def get_guava_jar(): | 
 |     return os.path.join( | 
 |         utils.REPO_ROOT, | 
 |         'third_party/gradle-plugin/com/google/guava/guava/22.0/guava-22.0.jar') | 
 |  | 
 |  | 
 | def get_sample_dir(app): | 
 |     return os.path.join(utils.REPO_ROOT, 'src', 'test', 'sampleApks', app) | 
 |  | 
 |  | 
 | def get_src_path(app): | 
 |     return os.path.join(get_sample_dir(app), 'src') | 
 |  | 
 |  | 
 | def get_dex_path(app): | 
 |     return os.path.join(get_bin_path(app), 'classes.dex') | 
 |  | 
 |  | 
 | def get_split_path(app, split): | 
 |     return os.path.join(get_bin_path(app), split, 'classes.dex') | 
 |  | 
 |  | 
 | def get_package_name(app): | 
 |     return '%s.%s' % (PACKAGE_PREFIX, app) | 
 |  | 
 |  | 
 | def get_qualified_activity(app): | 
 |     # The activity specified to adb start is PACKAGE_NAME/.ACTIVITY | 
 |     return '%s/.%s' % (get_package_name(app), STANDARD_ACTIVITY) | 
 |  | 
 |  | 
 | def run_aapt_pack(aapt, api, app): | 
 |     with utils.ChangedWorkingDirectory(get_sample_dir(app)): | 
 |         args = [ | 
 |             'package', '-v', '-f', '-I', | 
 |             utils.get_android_jar(api, 0), '-M', 'AndroidManifest.xml', '-A', | 
 |             'assets', '-S', 'res', '-m', '-J', | 
 |             get_gen_path(app), '-F', | 
 |             os.path.join(get_bin_path(app), 'resources.ap_'), '-G', | 
 |             os.path.join(get_build_dir(app), 'proguard_options') | 
 |         ] | 
 |         run_aapt(aapt, args) | 
 |  | 
 |  | 
 | def run_aapt_split_pack(aapt, api, app): | 
 |     with utils.ChangedWorkingDirectory(get_sample_dir(app)): | 
 |         args = [ | 
 |             'package', '-v', '-f', '-I', | 
 |             utils.get_android_jar(api, 0), '-M', | 
 |             'split_manifest/AndroidManifest.xml', '-S', 'res', '-F', | 
 |             os.path.join(get_bin_path(app), 'split_resources.ap_') | 
 |         ] | 
 |         run_aapt(aapt, args) | 
 |  | 
 |  | 
 | def compile_with_javac(api, app): | 
 |     with utils.ChangedWorkingDirectory(get_sample_dir(app)): | 
 |         files = glob.glob(SRC_LOCATION.format(app=app)) | 
 |         classpath = '%s:%s' % (utils.get_android_jar(api, 0), get_guava_jar()) | 
 |         command = [ | 
 |             DEFAULT_JAVAC, '-classpath', classpath, '-sourcepath', | 
 |             '%s:%s:%s' % | 
 |             (get_src_path(app), get_gen_path(app), get_guava_jar()), '-d', | 
 |             get_bin_path(app) | 
 |         ] | 
 |         command.extend(files) | 
 |         utils.PrintCmd(command) | 
 |         subprocess.check_call(command) | 
 |  | 
 |  | 
 | def dex(app, api): | 
 |     files = [] | 
 |     for root, dirnames, filenames in os.walk(get_bin_path(app)): | 
 |         for filename in fnmatch.filter(filenames, '*.class'): | 
 |             files.append(os.path.join(root, filename)) | 
 |     command = [ | 
 |         DEFAULT_D8, '--', '--output', | 
 |         get_bin_path(app), '--classpath', | 
 |         utils.get_android_jar(api, 0), '--min-api', | 
 |         str(api) | 
 |     ] | 
 |     command.extend(files) | 
 |     if app != 'simple': | 
 |         command.append(get_guava_jar()) | 
 |  | 
 |     utils.PrintCmd(command) | 
 |     subprocess.check_call(command) | 
 |  | 
 |  | 
 | def split(app): | 
 |     split_spec = os.path.join(get_sample_dir(app), 'split.spec') | 
 |     command = [ | 
 |         DEFAULT_DEXSPLITTER, '--input', | 
 |         get_dex_path(app), '--output', | 
 |         get_bin_path(app), '--feature-splits', split_spec | 
 |     ] | 
 |     utils.PrintCmd(command) | 
 |     subprocess.check_call(command) | 
 |  | 
 |  | 
 | def run_adb(args, ignore_exit=False): | 
 |     command = ['adb'] | 
 |     command.extend(args) | 
 |     utils.PrintCmd(command) | 
 |     # On M adb install-multiple exits 1 but succeed in installing. | 
 |     if ignore_exit: | 
 |         subprocess.call(command) | 
 |     else: | 
 |         subprocess.check_call(command) | 
 |  | 
 |  | 
 | def adb_install(apks): | 
 |     args = ['install-multiple' if len(apks) > 1 else 'install', '-r', '-d'] | 
 |     args.extend(apks) | 
 |     run_adb(args, ignore_exit=True) | 
 |  | 
 |  | 
 | def create_temp_apk(app, prefix): | 
 |     temp_apk_path = os.path.join(get_bin_path(app), '%s.ap_' % app) | 
 |     shutil.copyfile(os.path.join(get_bin_path(app), '%sresources.ap_' % prefix), | 
 |                     temp_apk_path) | 
 |     return temp_apk_path | 
 |  | 
 |  | 
 | def aapt_add_dex(aapt, dex, temp_apk_path): | 
 |     args = ['add', '-k', temp_apk_path, dex] | 
 |     run_aapt(aapt, args) | 
 |  | 
 |  | 
 | def kill(app): | 
 |     args = ['shell', 'am', 'force-stop', get_package_name(app)] | 
 |     run_adb(args) | 
 |  | 
 |  | 
 | def start_logcat(): | 
 |     return subprocess.Popen(['adb', 'logcat'], | 
 |                             bufsize=1024 * 1024, | 
 |                             stdout=subprocess.PIPE, | 
 |                             stderr=subprocess.PIPE) | 
 |  | 
 |  | 
 | def start(app): | 
 |     args = ['shell', 'am', 'start', '-n', get_qualified_activity(app)] | 
 |     run_adb(args) | 
 |  | 
 |  | 
 | def clear_logcat(): | 
 |     args = ['logcat', '-c'] | 
 |     run_adb(args) | 
 |  | 
 |  | 
 | def stop_logcat(popen): | 
 |     popen.terminate() | 
 |     lines = [] | 
 |     for l in popen.stdout: | 
 |         if 'System.out' in l: | 
 |             lines.append(l) | 
 |     return lines | 
 |  | 
 |  | 
 | def store_or_print_benchmarks(lines, output): | 
 |     results = {} | 
 |     overall_total = 0 | 
 |     # We assume that the total times are | 
 |     # prefixed with 'NAME Total: '. The logcat lines looks like: | 
 |     # 06-28 12:22:00.991 13698 13698 I System.out: Call Total: 61614 | 
 |     for l in lines: | 
 |         if 'Total: ' in l: | 
 |             split = l.split('Total: ') | 
 |             time = split[1] | 
 |             name = split[0].split()[-1] | 
 |             overall_total += int(time) | 
 |             print('%s: %s' % (name, time)) | 
 |             results[name] = time | 
 |  | 
 |     print('Total: %s' % overall_total) | 
 |     if not output: | 
 |         return overall_total | 
 |     results['total'] = str(overall_total) | 
 |     output_dir = os.path.join(output, str(uuid.uuid4())) | 
 |     os.makedirs(output_dir) | 
 |     written_files = [] | 
 |     for name, time in results.iteritems(): | 
 |         total_file = os.path.join(output_dir, name) | 
 |         written_files.append(total_file) | 
 |         with open(total_file, 'w') as f: | 
 |             f.write(time) | 
 |  | 
 |     print('Result stored in: \n%s' % ('\n'.join(written_files))) | 
 |     return overall_total | 
 |  | 
 |  | 
 | def benchmark(app, output_dir): | 
 |     # Ensure app is not running | 
 |     kill(app) | 
 |     clear_logcat() | 
 |     logcat = start_logcat() | 
 |     start(app) | 
 |     # We could do better here by continiously parsing the logcat for a marker, but | 
 |     # this works nicely with the current setup. | 
 |     time.sleep(12) | 
 |     kill(app) | 
 |     return float(store_or_print_benchmarks(stop_logcat(logcat), output_dir)) | 
 |  | 
 |  | 
 | def ensure_no_logcat(): | 
 |     output = subprocess.check_output(['ps', 'aux']) | 
 |     if 'adb logcat' in output: | 
 |         raise Exception( | 
 |             'You have adb logcat running, please close it and rerun') | 
 |  | 
 |  | 
 | def generate_proto_apks(apks, options): | 
 |     proto_apks = [] | 
 |     for apk in apks: | 
 |         proto_apk = apk + '.proto' | 
 |         cmd = [ | 
 |             options.aapt2, 'convert', '-o', proto_apk, '--output-format', | 
 |             'proto', apk | 
 |         ] | 
 |         utils.PrintCmd(cmd) | 
 |         subprocess.check_call(cmd) | 
 |         proto_apks.append(proto_apk) | 
 |     return proto_apks | 
 |  | 
 |  | 
 | def Main(): | 
 |     (options, args) = parse_options() | 
 |     apks = [] | 
 |     is_split = options.split | 
 |     run_aapt_pack(options.aapt, options.api, options.app) | 
 |     if is_split: | 
 |         run_aapt_split_pack(options.aapt, options.api, options.app) | 
 |     compile_with_javac(options.api, options.app) | 
 |     dex(options.app, options.api) | 
 |     dex_files = {options.app: get_dex_path(options.app)} | 
 |     dex_path = get_dex_path(options.app) | 
 |     if is_split: | 
 |         split(options.app) | 
 |         dex_path = get_split_path(options.app, 'base') | 
 |     temp_apk_path = create_temp_apk(options.app, '') | 
 |     aapt_add_dex(options.aapt, dex_path, temp_apk_path) | 
 |     apk_path = os.path.join(get_bin_path(options.app), '%s.apk' % options.app) | 
 |     apk_utils.sign(temp_apk_path, apk_path, options.keystore) | 
 |     apks.append(apk_path) | 
 |     if is_split: | 
 |         split_temp_apk_path = create_temp_apk(options.app, 'split_') | 
 |         aapt_add_dex(options.aapt, get_split_path(options.app, 'split'), | 
 |                      temp_apk_path) | 
 |         split_apk_path = os.path.join(get_bin_path(options.app), | 
 |                                       'featuresplit.apk') | 
 |         apk_utils.sign(temp_apk_path, split_apk_path, options.keystore) | 
 |         apks.append(split_apk_path) | 
 |     if options.generate_proto_apk: | 
 |         proto_apks = generate_proto_apks(apks, options) | 
 |         print('Generated proto apks available at: %s' % ' '.join(proto_apks)) | 
 |     print('Generated apks available at: %s' % ' '.join(apks)) | 
 |     if options.install or options.benchmark: | 
 |         adb_install(apks) | 
 |     grand_total = 0 | 
 |     if options.benchmark: | 
 |         ensure_no_logcat() | 
 |         for _ in range(BENCHMARK_ITERATIONS): | 
 |             grand_total += benchmark(options.app, options.benchmark_output_dir) | 
 |     print('Combined average: %s' % (grand_total / BENCHMARK_ITERATIONS)) | 
 |  | 
 |  | 
 | if __name__ == '__main__': | 
 |     sys.exit(Main()) |