Add an option for output of the main dex list

Currently this is only in InternalOptions, and not wired to public options.

Also added a utility for mapping and sorting a main dex list file.

Change-Id: I97a2487997ec8e26e07b1a1ee323c5b4bc254235
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index d5cf1b5..8b57155 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -405,6 +405,13 @@
         outputApp.writeProguardSeeds(closer, seedsOut);
       }
     }
+    if (options.printMainDexList && outputApp.hasMainDexList()) {
+      try (Closer closer = Closer.create()) {
+        OutputStream mainDexOut =
+            openPathWithDefault(closer, options.printMainDexListFile, true, System.out);
+        outputApp.writeMainDexList(closer, mainDexOut);
+      }
+    }
   }
 
   private static OutputStream openPathWithDefault(Closer closer,
diff --git a/src/main/java/com/android/tools/r8/ReadMainDexList.java b/src/main/java/com/android/tools/r8/ReadMainDexList.java
new file mode 100644
index 0000000..3a76c1f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ReadMainDexList.java
@@ -0,0 +1,58 @@
+// Copyright (c) 2017, 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;
+
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.ProguardMapReader;
+import com.android.tools.r8.utils.FileUtils;
+import java.nio.file.Paths;
+import java.util.stream.Collectors;
+
+/**
+ * Utility for applying proguard map and sorting the main dex list.
+ */
+public class ReadMainDexList {
+
+  private String DOT_CLASS = ".class";
+
+  private String stripDotClass(String name) {
+    return name.endsWith(DOT_CLASS) ? name.substring(0, name.length() - DOT_CLASS.length()) : name;
+  }
+
+  private String addDotClass(String name) {
+    return name + DOT_CLASS;
+  }
+
+  private String deobfuscateClassName(String name, ClassNameMapper mapper) {
+    if (mapper == null) {
+      return name;
+    }
+    return mapper.deobfuscateClassName(name);
+  }
+
+  private void run(String[] args) throws Exception {
+    if (args.length != 1 && args.length != 2) {
+      System.out.println("Usage: command <main_dex_list> [<proguard_map>]");
+      System.exit(0);
+    }
+
+    final ClassNameMapper mapper =
+        args.length == 2 ? ProguardMapReader.mapperFromFile(Paths.get(args[1])) : null;
+
+    FileUtils.readTextFile(Paths.get(args[0]))
+        .stream()
+        .map(this::stripDotClass)
+        .map(name -> name.replace('/', '.'))
+        .map(name -> deobfuscateClassName(name, mapper))
+        .map(name -> name.replace('.', '/'))
+        .map(this::addDotClass)
+        .sorted()
+        .collect(Collectors.toList())
+        .forEach(System.out::println);
+  }
+
+  public static void main(String[] args) throws Exception {
+    new ReadMainDexList().run(args);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index db264ca..bba049c 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -12,11 +12,13 @@
 import com.android.tools.r8.graph.DexDebugInfo;
 import com.android.tools.r8.graph.DexEncodedArray;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.naming.MinifiedNameMapPrinter;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.PackageDistribution;
 import java.io.ByteArrayOutputStream;
@@ -141,6 +143,10 @@
       if (proguardSeedsData != null) {
         builder.setProguardSeedsData(proguardSeedsData);
       }
+      byte[] mainDexList = writeMainDexList();
+      if (mainDexList != null) {
+        builder.setMainDexListData(mainDexList);
+      }
       return builder.build();
     } finally {
       application.timing.end();
@@ -177,4 +183,22 @@
     }
     return null;
   }
+
+  private String mapMainDexListName(DexType type) {
+    return DescriptorUtils.descriptorToJavaType(namingLens.lookupDescriptor(type).toString())
+        .replace('.', '/') + ".class";
+  }
+
+  private byte[] writeMainDexList() throws IOException {
+    if (application.mainDexList.isEmpty()) {
+      return null;
+    }
+    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+    PrintWriter writer = new PrintWriter(bytes);
+    application.mainDexList.forEach(
+        type -> writer.println(mapMainDexListName(type))
+    );
+    writer.flush();
+    return bytes.toByteArray();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index db8adab..4415ea7 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -136,7 +136,7 @@
         } else {
           System.out.println(
               "WARNING: Application does not contain `"
-                  + type.toDescriptorString()
+                  + type.toSourceString()
                   + "` as referenced in main-dex-list.");
         }
         mainDexFile.commitTransaction();
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
index 9e9a1ad..504cc4a 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
@@ -59,10 +59,15 @@
     return canonicalizeSignature(new FieldSignature(field.name.toString(), type));
   }
 
+  /**
+   * Deobfuscate a class name.
+   *
+   * Returns the deobfuscated name if a mapping was found. Otherwise it returns the passed in name.
+   */
   public String deobfuscateClassName(String name) {
     ClassNaming classNaming = classNameMappings.get(name);
     if (classNaming == null) {
-      return null;
+      return name;
     }
     return classNaming.originalName;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 7ea2367..b4fdfe9 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -345,6 +345,12 @@
     out.write(ByteStreams.toByteArray(input));
   }
 
+  public void writeMainDexList(Closer closer, OutputStream out) throws IOException {
+    InputStream input = getMainDexList(closer);
+    assert input != null;
+    out.write(ByteStreams.toByteArray(input));
+  }
+
   private OpenOption[] openOptions(boolean overwrite) {
     return new OpenOption[]{
         overwrite ? StandardOpenOption.CREATE : StandardOpenOption.CREATE_NEW,
@@ -571,6 +577,14 @@
     }
 
     /**
+     * Set the main-dex list data.
+     */
+    public Builder setMainDexListData(byte[] content) {
+      mainDexList = content == null ? null : Resource.fromBytes(null, content);
+      return this;
+    }
+
+    /**
      * Build final AndroidApp.
      */
     public AndroidApp build() {
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index ec430fd..eae33d1 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -88,11 +88,7 @@
         String clazz = descriptor.substring(1, descriptor.length() - 1)
             .replace(DESCRIPTOR_PACKAGE_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
         String originalName =
-            classNameMapper == null ? null : classNameMapper.deobfuscateClassName(clazz);
-        if (originalName == null) {
-          // Class was not renamed, reconstruct the java name.
-          return clazz;
-        }
+            classNameMapper == null ? clazz : classNameMapper.deobfuscateClassName(clazz);
         return originalName;
       case '[':
         return descriptorToJavaType(descriptor.substring(1, descriptor.length()), classNameMapper)
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 15fc89c..ad5485a 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -66,6 +66,8 @@
   public Path seedsFile;
   public boolean printMapping;
   public Path printMappingFile;
+  public boolean printMainDexList;
+  public Path printMainDexListFile;
   public boolean ignoreMissingClasses = false;
   public boolean skipMinification = false;
   public String packagePrefix = "";
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 28d4872..3f31686 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -96,6 +96,15 @@
   /**
    * Compile an application with R8.
    */
+  protected AndroidApp compileWithR8(List<Class> classes, Consumer<InternalOptions> optionsConsumer)
+      throws CompilationException, ProguardRuleParserException, ExecutionException, IOException {
+    R8Command command = ToolHelper.prepareR8CommandBuilder(readClasses(classes)).build();
+    return ToolHelper.runR8(command, optionsConsumer);
+  }
+
+  /**
+   * Compile an application with R8.
+   */
   protected AndroidApp compileWithR8(AndroidApp app, Consumer<InternalOptions> optionsConsumer)
       throws CompilationException, ProguardRuleParserException, ExecutionException, IOException {
     R8Command command = ToolHelper.prepareR8CommandBuilder(app).build();
diff --git a/src/test/java/com/android/tools/r8/maindexlist/HelloWorldMain.java b/src/test/java/com/android/tools/r8/maindexlist/HelloWorldMain.java
new file mode 100644
index 0000000..b70e73f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/maindexlist/HelloWorldMain.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2017, 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.maindexlist;
+
+public class HelloWorldMain {
+
+  public static void main(String[] args) {
+    System.out.println("Hello, world!");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
new file mode 100644
index 0000000..f8ca2a8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2017, 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.maindexlist;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.FileUtils;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import org.junit.Test;
+
+public class MainDexListOutputTest extends TestBase {
+  @Test
+  public void testNoMainDex() throws Exception {
+    Path mainDexList = temp.newFile().toPath();
+    compileWithR8(ImmutableList.of(HelloWorldMain.class),
+        options -> {
+          options.printMainDexList = true;
+          options.printMainDexListFile = mainDexList;
+        });
+    // Empty main dex list.
+    assertEquals(0, FileUtils.readTextFile(mainDexList).size());
+  }
+
+  @Test
+  public void testWithMainDex() throws Exception {
+    Path mainDexRules = writeTextToTempFile(keepMainProguardConfiguration(HelloWorldMain.class));
+    R8Command command =
+        ToolHelper.prepareR8CommandBuilder(readClasses(HelloWorldMain.class))
+            .addMainDexRules(mainDexRules)
+            .build();
+    Path mainDexList = temp.newFile().toPath();
+    ToolHelper.runR8(command,
+        options -> {
+          options.printMainDexList = true;
+          options.printMainDexListFile = mainDexList;
+        });
+    // Main dex list with the single class.
+    assertEquals(
+        ImmutableList.of(HelloWorldMain.class.getTypeName().replace('.', '/') + ".class"),
+        FileUtils.readTextFile(mainDexList));
+  }
+}