Extend script for running shrinkers on Android Studio projects

Change-Id: I1f37f199a0eb0db564851a3ce7b962e8da57043c
diff --git a/tools/as_utils.py b/tools/as_utils.py
new file mode 100644
index 0000000..1eaa206
--- /dev/null
+++ b/tools/as_utils.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+# 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.
+
+from distutils.version import LooseVersion
+import os
+
+import utils
+
+def add_r8_dependency(checkout_dir):
+  build_file = os.path.join(checkout_dir, 'build.gradle')
+  assert os.path.isfile(build_file), 'Expected a file to be present at {}'.format(build_file)
+
+  with open(build_file) as f:
+    lines = f.readlines()
+
+  added_r8_dependency = False
+  is_inside_dependencies = False
+
+  with open(build_file, 'w') as f:
+    gradle_version = None
+    for line in lines:
+      stripped = line.strip()
+      if stripped == 'dependencies {':
+        assert not is_inside_dependencies, 'Unexpected line with \'dependencies {\''
+        is_inside_dependencies = True
+      if is_inside_dependencies:
+        if utils.R8_JAR in stripped:
+          added_r8_dependency = True
+        elif 'com.android.tools.build:gradle:' in stripped:
+          gradle_version = stripped[stripped.rindex(':')+1:-1]
+          if not added_r8_dependency:
+            indent = ''.ljust(line.index('classpath'))
+            f.write('{}classpath files(\'{}\')\n'.format(indent, utils.R8_JAR))
+            added_r8_dependency = True
+        elif stripped == '}':
+          is_inside_dependencies = False
+      f.write(line)
+
+  assert added_r8_dependency, 'Unable to add R8 as a dependency'
+  assert gradle_version
+  assert LooseVersion(gradle_version) >= LooseVersion('3.2'), (
+      'Unsupported gradle version: {} (must use at least gradle '
+          + 'version 3.2)').format(gradle_version)
+
+def remove_r8_dependency(checkout_dir):
+  build_file = os.path.join(checkout_dir, 'build.gradle')
+  assert os.path.isfile(build_file), 'Expected a file to be present at {}'.format(build_file)
+  with open(build_file) as f:
+    lines = f.readlines()
+  with open(build_file, 'w') as f:
+    for line in lines:
+      if utils.R8_JAR not in line:
+        f.write(line)
diff --git a/tools/run_on_as_app.py b/tools/run_on_as_app.py
index 25385dd..0c6ea6c 100755
--- a/tools/run_on_as_app.py
+++ b/tools/run_on_as_app.py
@@ -10,7 +10,13 @@
 import sys
 import utils
 
+import as_utils
+
 SHRINKERS = ['r8', 'r8full', 'proguard']
+WORKING_DIR = utils.BUILD
+
+if 'R8_BENCHMARK_DIR' in os.environ and os.path.isdir(os.environ['R8_BENCHMARK_DIR']):
+  WORKING_DIR = os.environ['R8_BENCHMARK_DIR']
 
 APPS = {
   # 'app-name': {
@@ -19,6 +25,10 @@
   #     'archives_base_name': ... (default same as app_module)
   #     'flavor': ... (default no flavor)
   # },
+  'AntennaPod': {
+      'git_repo': 'https://github.com/AntennaPod/AntennaPod.git',
+      'flavor': 'play',
+  },
   'tachiyomi': {
       'git_repo': 'https://github.com/sgjesse/tachiyomi.git',
       'flavor': 'standard',
@@ -31,6 +41,13 @@
   },
 }
 
+def IsBuiltWithR8(apk):
+  script = os.path.join(utils.TOOLS_DIR, 'extractmarker.py')
+  return '~~R8' in subprocess.check_output(['python', script, apk]).strip()
+
+def IsTrackedByGit(file):
+  return subprocess.check_output(['git', 'ls-files', file]).strip() != ''
+
 def GitClone(git_url):
   return subprocess.check_output(['git', 'clone', git_url]).strip()
 
@@ -40,15 +57,35 @@
 def GitCheckout(file):
   return subprocess.check_output(['git', 'checkout', file]).strip()
 
+def MoveApkToDest(apk, apk_dest):
+  print('Moving `{}` to `{}`'.format(apk, apk_dest))
+  assert os.path.isfile(apk)
+  if os.path.isfile(apk_dest):
+    os.remove(apk_dest)
+  os.rename(apk, apk_dest)
+
 def ParseOptions(argv):
   result = optparse.OptionParser()
   result.add_option('--app',
                     help='What app to run on',
                     choices=APPS.keys())
+  result.add_option('--sign_apks',
+                    help='Whether the APKs should be signed',
+                    default=False,
+                    action='store_true')
+  result.add_option('--shrinker',
+                    help='The shrinker to use (by default, all are run)',
+                    choices=SHRINKERS)
+  result.add_option('--use_tot',
+                    help='Whether to use the ToT version of R8',
+                    default=False,
+                    action='store_true')
   return result.parse_args(argv)
 
 def main(argv):
   (options, args) = ParseOptions(argv)
+  assert not options.use_tot or os.path.isfile(utils.R8_JAR), (
+      'Cannot build from ToT without r8.jar')
 
   # Common environment setup.
   user_home = os.path.expanduser('~')
@@ -69,26 +106,38 @@
   flavor = config.get('flavor')
 
   # Checkout and build in the build directory.
-  working_dir = utils.BUILD
-  checkout_dir = os.path.join(working_dir, app)
+  checkout_dir = os.path.join(WORKING_DIR, app)
 
   if not os.path.exists(checkout_dir):
-    with utils.ChangedWorkingDirectory(working_dir):
+    with utils.ChangedWorkingDirectory(WORKING_DIR):
       GitClone(git_repo)
   else:
     with utils.ChangedWorkingDirectory(checkout_dir):
       GitPull()
 
+  if options.use_tot:
+    as_utils.add_r8_dependency(checkout_dir)
+  else:
+    as_utils.remove_r8_dependency(checkout_dir)
+
   with utils.ChangedWorkingDirectory(checkout_dir):
     for shrinker in SHRINKERS:
+      if options.shrinker is not None and shrinker != options.shrinker:
+        continue
+
+      print('Building with {}'.format(shrinker))
 
       # Ensure that gradle.properties are not modified before modifying it to
       # select shrinker.
-      GitCheckout('gradle.properties')
+      if IsTrackedByGit('gradle.properties'):
+        GitCheckout('gradle.properties')
       with open("gradle.properties", "a") as gradle_properties:
-        if shrinker == 'r8full':
-          gradle_properties.write('\nandroid.enableR8.fullMode=true\n')
-        if shrinker == 'proguard':
+        if 'r8' in shrinker:
+          gradle_properties.write('\nandroid.enableR8=true\n')
+          if shrinker == 'r8full':
+            gradle_properties.write('android.enableR8.fullMode=true\n')
+        else:
+          assert shrinker == 'proguard'
           gradle_properties.write('\nandroid.enableR8=false\n')
 
       out = os.path.join(checkout_dir, 'out', shrinker)
@@ -97,35 +146,51 @@
 
       env = os.environ.copy()
       env['ANDROID_HOME'] = android_home
-      releaseTarget = app_module + ':' + 'assembleRelease'
-      subprocess.check_call(
-          ['./gradlew', '--no-daemon', 'clean', releaseTarget], env=env)
+      env['JAVA_OPTS'] = '-ea'
+      releaseTarget = app_module + ':' + 'assemble' + (
+          flavor.capitalize() if flavor else '') + 'Release'
 
-      unsigned_apk_name = (archives_base_name
-          + (('-' + flavor) if flavor else '')
-          + '-release-unsigned.apk')
-      signed_apk_name = archives_base_name + '-release.apk'
+      cmd = ['./gradlew', '--no-daemon', 'clean', releaseTarget, '--stacktrace']
+      utils.PrintCmd(cmd)
+      subprocess.check_call(cmd, env=env)
+
+      apk_base_name = (archives_base_name
+          + (('-' + flavor) if flavor else '') + '-release')
+      signed_apk_name = apk_base_name + '.apk'
+      unsigned_apk_name = apk_base_name + '-unsigned.apk'
 
       build_output_apks = os.path.join(app_module, 'build', 'outputs', 'apk')
       if flavor:
-        unsigned_apk = os.path.join(
-            build_output_apks, flavor, 'release', unsigned_apk_name)
+        build_output_apks = os.path.join(build_output_apks, flavor, 'release')
       else:
-        unsigned_apk = os.path.join(
-            build_output_apks, 'release', unsigned_apk_name)
+        build_output_apks = os.path.join(build_output_apks, 'release')
 
-      signed_apk = os.path.join(out, signed_apk_name)
+      signed_apk = os.path.join(build_output_apks, signed_apk_name)
+      unsigned_apk = os.path.join(build_output_apks, unsigned_apk_name)
 
-      keystore = 'app.keystore'
-      keystore_password = 'android'
-      apk_utils.sign_with_apksigner(
-          android_build_tools,
-          unsigned_apk,
-          signed_apk,
-          keystore,
-          keystore_password)
+      if options.sign_apks and not os.path.isfile(signed_apk):
+        assert os.path.isfile(unsigned_apk)
+        if options.sign_apks:
+          keystore = 'app.keystore'
+          keystore_password = 'android'
+          apk_utils.sign_with_apksigner(
+              android_build_tools,
+              unsigned_apk,
+              signed_apk,
+              keystore,
+              keystore_password)
 
-    GitCheckout('gradle.properties')
+      if os.path.isfile(signed_apk):
+        apk_dest = os.path.join(out, signed_apk_name)
+        MoveApkToDest(signed_apk, apk_dest)
+      else:
+        apk_dest = os.path.join(out, unsigned_apk_name)
+        MoveApkToDest(unsigned_apk, apk_dest)
+
+      assert IsBuiltWithR8(apk_dest) == ('r8' in shrinker)
+
+    if IsTrackedByGit('gradle.properties'):
+      GitCheckout('gradle.properties')
 
 if __name__ == '__main__':
   sys.exit(main(sys.argv[1:]))