|  | #!/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), '-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), '-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), 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), '--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()) |