Add command line utility for compile dump files.

Change-Id: Iace6b27561a01e5e55386a346bc0284ff9471fb6
diff --git a/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java b/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java
new file mode 100644
index 0000000..271bd70
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java
@@ -0,0 +1,128 @@
+// Copyright (c) 2020, 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.
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.CompatProguardCommandBuilder;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Wrapper to make it easy to call R8 in compat mode when compiling a dump file.
+ *
+ * <p>This wrapper will be added to the classpath so it *must* only refer to the public API. See
+ * {@code tools/compiledump.py}.
+ *
+ * <p>It is tempting to have this class share the R8 parser code, but such refactoring would not be
+ * valid on past version of the R8 API. Thus there is little else to do than reimplement the parts
+ * we want to support for reading dumps.
+ */
+public class CompileDumpCompatR8 {
+
+  private static final List<String> VALID_OPTIONS =
+      Arrays.asList("--classfile", "--compat", "--debug", "--release");
+
+  private static final List<String> VALID_OPTIONS_WITH_OPERAND =
+      Arrays.asList(
+          "--output",
+          "--lib",
+          "--classpath",
+          "--min-api",
+          "--main-dex-rules",
+          "--main-dex-list",
+          "--main-dex-list-output",
+          "--pg-conf",
+          "--pg-map-output",
+          "--desugared-lib");
+
+  public static void main(String[] args) throws CompilationFailedException {
+    boolean isCompatMode = false;
+    OutputMode outputMode = OutputMode.DexIndexed;
+    Path outputPath = null;
+    CompilationMode compilationMode = CompilationMode.RELEASE;
+    List<Path> program = new ArrayList<>();
+    List<Path> library = new ArrayList<>();
+    List<Path> classpath = new ArrayList<>();
+    List<Path> config = new ArrayList<>();
+    int minApi = 1;
+    for (int i = 0; i < args.length; i++) {
+      String option = args[i];
+      if (VALID_OPTIONS.contains(option)) {
+        switch (option) {
+          case "--classfile":
+            {
+              outputMode = OutputMode.ClassFile;
+              break;
+            }
+          case "--compat":
+            {
+              isCompatMode = true;
+              break;
+            }
+          case "--debug":
+            {
+              compilationMode = CompilationMode.DEBUG;
+              break;
+            }
+          case "--release":
+            {
+              compilationMode = CompilationMode.RELEASE;
+              break;
+            }
+          default:
+            throw new IllegalArgumentException("Unimplemented option: " + option);
+        }
+      } else if (VALID_OPTIONS_WITH_OPERAND.contains(option)) {
+        String operand = args[++i];
+        switch (option) {
+          case "--output":
+            {
+              outputPath = Paths.get(operand);
+              break;
+            }
+          case "--lib":
+            {
+              library.add(Paths.get(operand));
+              break;
+            }
+          case "--classpath":
+            {
+              classpath.add(Paths.get(operand));
+              break;
+            }
+          case "--min-api":
+            {
+              minApi = Integer.parseInt(operand);
+              break;
+            }
+          case "--pg-conf":
+            {
+              config.add(Paths.get(operand));
+              break;
+            }
+          default:
+            throw new IllegalArgumentException("Unimplemented option: " + option);
+        }
+      } else {
+        program.add(Paths.get(option));
+      }
+    }
+    R8.run(
+        new CompatProguardCommandBuilder(isCompatMode)
+            .addProgramFiles(program)
+            .addLibraryFiles(library)
+            .addClasspathFiles(classpath)
+            .addProguardConfigurationFiles(config)
+            .setOutput(outputPath, outputMode)
+            .setMode(compilationMode)
+            .setMinApiLevel(minApi)
+            .build());
+  }
+}
diff --git a/tools/compiledump.py b/tools/compiledump.py
new file mode 100755
index 0000000..6f78eef
--- /dev/null
+++ b/tools/compiledump.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python
+# Copyright (c) 2020, 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.
+
+import argparse
+import os
+import subprocess
+import sys
+import zipfile
+
+import archive
+import jdk
+import retrace
+import utils
+
+
+def make_parser():
+  parser = argparse.ArgumentParser(description = 'Compile a dump artifact.')
+  parser.add_argument(
+    '-d',
+    '--dump',
+    help='Dump file to compile',
+    default=None)
+  parser.add_argument(
+    '-c',
+    '--compiler',
+    help='Compiler to use (default read from version file)',
+    default=None)
+  parser.add_argument(
+    '-v',
+    '--version',
+    help='Compiler version to use (default read from dump version file).'
+      'Valid arguments are:'
+      '  "master" to run from your own tree,'
+      '  "X.Y.Z" to run a specific version, or'
+      '  <hash> to run that hash from master.',
+    default=None)
+  parser.add_argument(
+    '--nolib',
+    help='Use the non-lib distribution (default uses the lib distribution)',
+    default=False,
+    action='store_true')
+  parser.add_argument(
+    '--ea',
+    help='Enable Java assertions when running the compiler (default disabled)',
+    default=False,
+    action='store_true')
+  parser.add_argument(
+      '--debug-agent',
+      help='Enable Java debug agent and suspend compilation (default disabled)',
+      default=False,
+      action='store_true')
+  return parser
+
+def error(msg):
+  print msg
+  sys.exit(1)
+
+class Dump(object):
+
+  def __init__(self, directory):
+    self.directory = directory
+
+  def if_exists(self, name):
+    f = os.path.join(self.directory, name)
+    if os.path.exists(f):
+      return f
+    return None
+
+  def program_jar(self):
+    return self.if_exists('program.jar')
+
+  def library_jar(self):
+    return self.if_exists('library.jar')
+
+  def classpath_jar(self):
+    return self.if_exists('classpath.jar')
+
+  def config_file(self):
+    return self.if_exists('proguard.config')
+
+  def version_file(self):
+    return self.if_exists('r8-version')
+
+  def version(self):
+    f = self.version_file()
+    if f:
+      return open(f).read().split(' ')[0]
+    return None
+
+def read_dump(args, temp):
+  if args.dump is None:
+    error("A dump file must be specified")
+  dump_file = zipfile.ZipFile(args.dump, 'r')
+  with utils.ChangedWorkingDirectory(temp):
+    dump_file.extractall()
+    return Dump(temp)
+
+def determine_version(args, dump):
+  if args.version is None:
+    return dump.version()
+  return args.version
+
+def determine_compiler(args, dump):
+  compilers = ('d8', 'r8', 'r8full')
+  if args.compiler not in compilers:
+    error("Unable to determine a compiler to use. Valid options: %s" % compilers.join(', '))
+  return args.compiler
+
+def determine_output(args, temp):
+  return os.path.join(temp, 'out.jar')
+
+def download_distribution(args, version, temp):
+  if version == 'master':
+    return utils.R8_JAR if args.nolib else utils.R8LIB_JAR
+  name = 'r8.jar' if args.nolib else 'r8lib.jar'
+  source = archive.GetUploadDestination(version, name, is_hash(version))
+  dest = os.path.join(temp, 'r8.jar')
+  utils.download_file_from_cloud_storage(source, dest)
+  return dest
+
+def prepare_wrapper(dist, temp):
+  wrapper_file = os.path.join(
+      utils.REPO_ROOT,
+      'src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java')
+  subprocess.check_output([
+    jdk.GetJavacExecutable(),
+    wrapper_file,
+    '-d', temp,
+    '-cp', dist,
+    ])
+  return temp
+
+def is_hash(version):
+  return len(version) == 40
+
+def run(args, otherargs):
+  with utils.TempDir() as temp:
+    dump = read_dump(args, temp)
+    version = determine_version(args, dump)
+    compiler = determine_compiler(args, dump)
+    out = determine_output(args, temp)
+    jar = download_distribution(args, version, temp)
+    wrapper_dir = prepare_wrapper(jar, temp)
+    cmd = [jdk.GetJavaExecutable()]
+    if args.debug_agent:
+      if not args.nolib:
+        print "WARNING: Running debugging agent on r8lib is questionable..."
+      cmd.append(
+          '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005')
+    if args.ea:
+      cmd.append('-ea')
+    cmd.extend(['-cp', '%s:%s' % (wrapper_dir, jar)])
+    if compiler == 'd8':
+      cmd.append('com.android.tools.r8.D8')
+    if compiler.startswith('r8'):
+      cmd.append('com.android.tools.r8.utils.CompileDumpCompatR8')
+    if compiler == 'r8':
+      cmd.append('--compat')
+    cmd.append(dump.program_jar())
+    cmd.extend(['--output', out])
+    if dump.library_jar():
+      cmd.extend(['--lib', dump.library_jar()])
+    if dump.classpath_jar():
+      cmd.extend(['--classpath', dump.classpath_jar()])
+    if compiler != 'd8' and dump.config_file():
+      cmd.extend(['--pg-conf', dump.config_file()])
+    cmd.extend(otherargs)
+    utils.PrintCmd(cmd)
+    try:
+      print subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+      return 0
+    except subprocess.CalledProcessError, e:
+      print e.output
+      if not args.nolib:
+        stacktrace = os.path.join(temp, 'stacktrace')
+        open(stacktrace, 'w+').write(e.output)
+        local_map = utils.R8LIB_MAP if version == 'master' else None
+        hash_or_version = None if version == 'master' else version
+        print "=" * 80
+        print " RETRACED OUTPUT"
+        print "=" * 80
+        retrace.run(local_map, hash_or_version, stacktrace, is_hash(version))
+      return 1
+
+if __name__ == '__main__':
+  (args, otherargs) = make_parser().parse_known_args(sys.argv[1:])
+  sys.exit(run(args, otherargs))
diff --git a/tools/retrace.py b/tools/retrace.py
index e8fdf89..771f23d 100755
--- a/tools/retrace.py
+++ b/tools/retrace.py
@@ -53,16 +53,18 @@
 
 def main():
   args = parse_arguments()
-  r8lib_map_path = args.map
   if args.tag:
     hash_or_version = find_version_or_hash_from_tag(args.tag)
   else:
     hash_or_version = args.commit_hash or args.version
+  return run(args.map, hash_or_version, args.stacktrace, args.commit_hash is not None)
+
+def run(r8lib_map_path, hash_or_version, stacktrace, is_hash):
   if hash_or_version:
     download_path = archive.GetUploadDestination(
         hash_or_version,
         'r8lib.jar.map',
-        args.commit_hash is not None)
+        is_hash)
     if utils.file_exists_on_cloud_storage(download_path):
       r8lib_map_path = tempfile.NamedTemporaryFile().name
       utils.download_file_from_cloud_storage(download_path, r8lib_map_path)
@@ -78,8 +80,8 @@
     r8lib_map_path
   ]
 
-  if args.stacktrace:
-    retrace_args.append(args.stacktrace)
+  if stacktrace:
+    retrace_args.append(stacktrace)
 
   return subprocess.call(retrace_args)
 
diff --git a/tools/utils.py b/tools/utils.py
index 6690ba2..16a0696 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -46,6 +46,7 @@
 D8_JAR = os.path.join(LIBS, 'd8.jar')
 R8_JAR = os.path.join(LIBS, 'r8.jar')
 R8LIB_JAR = os.path.join(LIBS, 'r8lib.jar')
+R8LIB_MAP = os.path.join(LIBS, 'r8lib.jar.map')
 R8_SRC_JAR = os.path.join(LIBS, 'r8-src.jar')
 R8LIB_EXCLUDE_DEPS_JAR = os.path.join(LIBS, 'r8lib-exclude-deps.jar')
 R8_FULL_EXCLUDE_DEPS_JAR = os.path.join(LIBS, 'r8-full-exclude-deps.jar')