Update the main-dex handling for D8 and R8

* Both D8 and R8 can now take several main-dex files.
* In the API main-dex classes can be specified using strings as well
  as through files.
* R8 have an explicit option for outputting the computed main-dex list.

Added the APIs

  BaseCommand.addMainDexListFiles(Path... files)
  BaseCommand.addMainDexListFiles(Collection<Path> files)
  BaseCommand.addMainDexClass(String clazz)
  R8Command.setMainDexListOutputPath(Path mainDexListOutputPath)

Removed the API

  BaseCommand.setMainDexListFile(Path file)

D8 and R8 can take several --main-dex-list options
R8 have the new option --main-dex-list-output

Change-Id: Ibca32c74a193c6961ad3980d92f61b0561d02272
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java
index 5eb67c4..430624b 100644
--- a/src/main/java/com/android/tools/r8/BaseCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -219,9 +219,53 @@
       return self();
     }
 
-    /** Set the main-dex list file. */
-    public B setMainDexListFile(Path file) {
-      app.setMainDexListFile(file);
+    /**
+     * Add main-dex list files.
+     *
+     * Each line in each of the files specifies one class to keep in the primary dex file
+     * (<code>classes.dex</code>).
+     *
+     * A class is specified using the following format: "com/example/MyClass.class". That is
+     * "/" as separator between package components, and a trailing ".class".
+     */
+    public B addMainDexListFiles(Path... files) throws IOException {
+      app.addMainDexListFiles(files);
+      return self();
+    }
+
+    /**
+     * Add main-dex list files.
+     *
+     * @see #addMainDexListFiles(Path...)
+     */
+    public B addMainDexListFiles(Collection<Path> files) throws IOException {
+      app.addMainDexListFiles(files);
+      return self();
+    }
+
+    /**
+     * Add main-dex classes.
+     *
+     * Add classes to keep in the primary dex file (<code>classes.dex</code>).
+     *
+     * NOTE: The name of the classes is specified using the Java fully qualified names format
+     * (e.g. "com.example.MyClass"), and <i>not</i> the format used by the main-dex list file.
+     */
+    public B addMainDexClasses(String... classes) {
+      app.addMainDexClasses(classes);
+      return self();
+    }
+
+    /**
+     * Add main-dex classes.
+     *
+     * Add classes to keep in the primary dex file (<code>classes.dex</code>).
+     *
+     * NOTE: The name of the classes is specified using the Java fully qualified names format
+     * (e.g. "com.example.MyClass"), and <i>not</i> the format used by the main-dex list file.
+     */
+    public B addMainDexClasses(Collection<String> classes) {
+      app.addMainDexClasses(classes);
       return self();
     }
 
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index b856556..4c9b542 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -131,7 +131,6 @@
   public static Builder parse(String[] args) throws CompilationException, IOException {
     CompilationMode modeSet = null;
     Path outputPath = null;
-    String mainDexList = null;
     Builder builder = builder();
     try {
       for (int i = 0; i < args.length; i++) {
@@ -168,11 +167,7 @@
         } else if (arg.equals("--classpath")) {
           builder.addClasspathFiles(Paths.get(args[++i]));
         } else if (arg.equals("--main-dex-list")) {
-          if (mainDexList != null) {
-            throw new CompilationException("Only one --main-dex-list supported");
-          }
-          mainDexList = args[++i];
-          builder.setMainDexListFile(Paths.get(mainDexList));
+          builder.addMainDexListFiles(Paths.get(args[++i]));
         } else if (arg.equals("--min-api")) {
           builder.setMinApiLevel(Integer.valueOf(args[++i]));
         } else if (arg.equals("--intermediate")) {
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 50f79dc..5f56d79 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -432,7 +432,7 @@
         outputApp.writeProguardSeeds(closer, seedsOut);
       }
     }
-    if (options.printMainDexList && outputApp.hasMainDexList()) {
+    if (outputApp.hasMainDexListOutput()) {
       try (Closer closer = Closer.create()) {
         OutputStream mainDexOut =
             FileUtils.openPathWithDefault(
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 8d3d937..241ea85 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -27,6 +27,7 @@
 
     private final List<Path> mainDexRules = new ArrayList<>();
     private boolean minimalMainDex = false;
+    private Path mainDexListOutput = null;
     private final List<Path> proguardConfigFiles = new ArrayList<>();
     private Optional<Boolean> treeShaking = Optional.empty();
     private Optional<Boolean> minification = Optional.empty();
@@ -88,6 +89,11 @@
       return self();
     }
 
+    public Builder setMainDexListOutputPath(Path mainDexListOutputPath) {
+      mainDexListOutput = mainDexListOutputPath;
+      return self();
+    }
+
     /**
      * Add proguard configuration file resources.
      */
@@ -134,7 +140,11 @@
       super.validate();
       if (minimalMainDex && mainDexRules.isEmpty() && !getAppBuilder().hasMainDexList()) {
         throw new CompilationException(
-            "Option --minimal-main-dex require --main-dex-rules");
+            "Option --minimal-main-dex require --main-dex-rules and/or --main-dex-list");
+      }
+      if (mainDexListOutput != null && mainDexRules.isEmpty() && !getAppBuilder().hasMainDexList()) {
+        throw new CompilationException(
+            "Option --main-dex-list-output require --main-dex-rules and/or --main-dex-list");
       }
     }
 
@@ -183,6 +193,7 @@
           getOutputMode(),
           mainDexKeepRules,
           minimalMainDex,
+          mainDexListOutput,
           configuration,
           getMode(),
           getMinApiLevel(),
@@ -215,13 +226,16 @@
       "  --no-minification        # Force disable minification of names.",
       "  --main-dex-rules <file>  # Proguard keep rules for classes to place in the",
       "                           # primary dex file.",
+      "  --main-dex-list <file>   # List of classes to place in the primary dex file.",
       "  --minimal-main-dex       # Only place classes specified by --main-dex-rules",
       "                           # in the primary dex file.",
+      "  --main-dex-list-output <file>  # Output the full main-dex list in <file>.",
       "  --version                # Print the version of r8.",
       "  --help                   # Print this message."));
 
   private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
   private final boolean minimalMainDex;
+  private final Path mainDexListOutput;
   private final ProguardConfiguration proguardConfiguration;
   private final boolean useTreeShaking;
   private final boolean useMinification;
@@ -285,8 +299,12 @@
         builder.setMinification(false);
       } else if (arg.equals("--main-dex-rules")) {
         builder.addMainDexRules(Paths.get(args[++i]));
+      } else if (arg.equals("--main-dex-list")) {
+        builder.addMainDexListFiles(Paths.get(args[++i]));
       } else if (arg.equals("--minimal-main-dex")) {
         builder.setMinimalMainDex(true);
+      } else if (arg.equals("--main-dex-list-output")) {
+        builder.setMainDexListOutputPath(Paths.get(args[++i]));
       } else if (arg.equals("--pg-conf")) {
         builder.addProguardConfigurationFiles(Paths.get(args[++i]));
       } else if (arg.equals("--pg-map")) {
@@ -329,6 +347,7 @@
       OutputMode outputMode,
       ImmutableList<ProguardConfigurationRule> mainDexKeepRules,
       boolean minimalMainDex,
+      Path mainDexListOutput,
       ProguardConfiguration proguardConfiguration,
       CompilationMode mode,
       int minApiLevel,
@@ -341,6 +360,7 @@
     assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
     this.mainDexKeepRules = mainDexKeepRules;
     this.minimalMainDex = minimalMainDex;
+    this.mainDexListOutput = mainDexListOutput;
     this.proguardConfiguration = proguardConfiguration;
     this.useTreeShaking = useTreeShaking;
     this.useMinification = useMinification;
@@ -351,6 +371,7 @@
     super(printHelp, printVersion);
     mainDexKeepRules = ImmutableList.of();
     minimalMainDex = false;
+    mainDexListOutput = null;
     proguardConfiguration = null;
     useTreeShaking = false;
     useMinification = false;
@@ -410,6 +431,9 @@
     internal.obfuscationDictionary = proguardConfiguration.getObfuscationDictionary();
     internal.mainDexKeepRules = mainDexKeepRules;
     internal.minimalMainDex = minimalMainDex;
+    if (mainDexListOutput != null) {
+      internal.printMainDexListFile = mainDexListOutput;
+    }
     internal.keepRules = proguardConfiguration.getRules();
     internal.dontWarnPatterns = proguardConfiguration.getDontWarnPatterns();
     internal.outputMode = getOutputMode();
diff --git a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
index 6bbb406..6750f4c 100644
--- a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
+++ b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
@@ -450,13 +450,15 @@
     ExecutorService executor = ThreadUtils.getExecutorService(numberOfThreads);
     D8Output result;
     try {
-       result = D8.run(
-          D8Command.builder()
-              .addProgramFiles(inputs)
-              .setMode(mode)
-              .setMinApiLevel(dexArgs.minApiLevel)
-              .setMainDexListFile(mainDexList)
-              .build());
+      D8Command.Builder builder = D8Command.builder();
+      builder
+          .addProgramFiles(inputs)
+          .setMode(mode)
+          .setMinApiLevel(dexArgs.minApiLevel);
+      if (mainDexList != null) {
+        builder.addMainDexListFiles(mainDexList);
+      }
+      result = D8.run(builder.build());
     } finally {
       executor.shutdown();
     }
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index 355080f..8820b4d 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -29,6 +29,7 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ClassProvider;
 import com.android.tools.r8.utils.ClasspathClassCollection;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.LibraryClassCollection;
 import com.android.tools.r8.utils.MainDexList;
@@ -45,6 +46,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
+import java.util.stream.Collectors;
 
 public class ApplicationReader {
 
@@ -149,13 +151,20 @@
     if (inputApp.hasMainDexList()) {
       futures.add(executorService.submit(() -> {
         try {
-          InputStream input = inputApp.getMainDexList(closer);
-          builder.addToMainDexList(MainDexList.parse(input, itemFactory));
+          for (Resource resource : inputApp.getMainDexListResources()) {
+            InputStream input = closer.register(resource.getStream());
+            builder.addToMainDexList(MainDexList.parse(input, itemFactory));
+          }
         } catch (IOException e) {
           throw new RuntimeException(e);
         }
       }));
     }
+    builder.addToMainDexList(
+        inputApp.getMainDexClasses()
+            .stream()
+            .map(clazz -> itemFactory.createType(DescriptorUtils.javaTypeToDescriptor(clazz)))
+            .collect(Collectors.toList()));
   }
 
   private final class ClassReader {
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 5613169..1cb0fb0 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -186,7 +186,7 @@
       }
       byte[] mainDexList = writeMainDexList();
       if (mainDexList != null) {
-        builder.setMainDexListData(mainDexList);
+        builder.setMainDexListOutputData(mainDexList);
       }
       return builder.build();
     } finally {
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 b328895..a297c10 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -52,7 +52,9 @@
   private final Resource proguardMap;
   private final Resource proguardSeeds;
   private final Resource packageDistribution;
-  private final Resource mainDexList;
+  private final List<Resource> mainDexListResources;
+  private final List<String> mainDexClasses;
+  private final Resource mainDexListOutput;
 
   // See factory methods and AndroidApp.Builder below.
   private AndroidApp(
@@ -64,7 +66,9 @@
       Resource proguardMap,
       Resource proguardSeeds,
       Resource packageDistribution,
-      Resource mainDexList) {
+      List<Resource> mainDexListResources,
+      List<String> mainDexClasses,
+      Resource mainDexListOutput) {
     this.programResources = programResources;
     this.programFileArchiveReaders = programFileArchiveReaders;
     this.classpathResourceProviders = classpathResourceProviders;
@@ -73,7 +77,9 @@
     this.proguardMap = proguardMap;
     this.proguardSeeds = proguardSeeds;
     this.packageDistribution = packageDistribution;
-    this.mainDexList = mainDexList;
+    this.mainDexListResources = mainDexListResources;
+    this.mainDexClasses = mainDexClasses;
+    this.mainDexListOutput = mainDexListOutput;
   }
 
   /**
@@ -242,14 +248,35 @@
    * True if the main dex list resource exists.
    */
   public boolean hasMainDexList() {
-    return mainDexList != null;
+    return !mainDexListResources.isEmpty();
   }
 
   /**
-   * Get the input stream of the main dex list resource if it exists.
+   * Get the main dex list resources if any.
    */
-  public InputStream getMainDexList(Closer closer) throws IOException {
-    return mainDexList == null ? null : closer.register(mainDexList.getStream());
+  public List<Resource> getMainDexListResources() {
+    return mainDexListResources;
+  }
+
+  /**
+   * Get the main dex classes if any.
+   */
+  public List<String> getMainDexClasses() {
+    return mainDexClasses;
+  }
+
+  /**
+   * True if the main dex list resource exists.
+   */
+  public boolean hasMainDexListOutput() {
+    return mainDexListOutput != null;
+  }
+
+  /**
+   * Get the main dex list output resources if any.
+   */
+  public InputStream getMainDexListOutput(Closer closer) throws IOException {
+    return mainDexListOutput == null ? null : closer.register(mainDexListOutput.getStream());
   }
 
   /**
@@ -361,7 +388,7 @@
   }
 
   public void writeMainDexList(Closer closer, OutputStream out) throws IOException {
-    InputStream input = getMainDexList(closer);
+    InputStream input = getMainDexListOutput(closer);
     assert input != null;
     out.write(ByteStreams.toByteArray(input));
   }
@@ -385,7 +412,9 @@
     private Resource proguardMap;
     private Resource proguardSeeds;
     private Resource packageDistribution;
-    private Resource mainDexList;
+    private List<Resource> mainDexListResources = new ArrayList<>();
+    private List<String> mainDexListClasses = new ArrayList<>();
+    private Resource mainDexListOutput;
 
     // See AndroidApp::builder().
     private Builder() {
@@ -401,7 +430,9 @@
       proguardMap = app.proguardMap;
       proguardSeeds = app.proguardSeeds;
       packageDistribution = app.packageDistribution;
-      mainDexList = app.mainDexList;
+      mainDexListResources = app.mainDexListResources;
+      mainDexListClasses = app.mainDexClasses;
+      mainDexListOutput = app.mainDexListOutput;
     }
 
     /**
@@ -585,22 +616,49 @@
     }
 
     /**
-     * Set the main-dex list file.
+     * Add a main-dex list file.
      */
-    public Builder setMainDexListFile(Path file) {
-      mainDexList = file == null ? null : Resource.fromFile(null, file);
+    public Builder addMainDexListFiles(Path... files) throws IOException {
+      return addMainDexListFiles(Arrays.asList(files));
+    }
+
+    public Builder addMainDexListFiles(Collection<Path> files) throws IOException {
+      for (Path file : files) {
+        if (!Files.exists(file)) {
+          throw new FileNotFoundException("Non-existent input file: " + file);
+        }
+        // TODO(sgjesse): Should we just read the file here? This will sacrifice the parallelism
+        // in ApplicationReader where all input resources are read in parallel.
+        mainDexListResources.add(Resource.fromFile(null, file));
+      }
+      return this;
+    }
+
+
+    /**
+     * Add main-dex classes.
+     */
+    public Builder addMainDexClasses(String... classes) {
+      return addMainDexClasses(Arrays.asList(classes));
+    }
+
+    /**
+     * Add main-dex classes.
+     */
+    public Builder addMainDexClasses(Collection<String> classes) {
+      mainDexListClasses.addAll(classes);
       return this;
     }
 
     public boolean hasMainDexList() {
-      return mainDexList != null;
+      return !(mainDexListResources.isEmpty() && mainDexListClasses.isEmpty());
     }
 
     /**
-     * Set the main-dex list data.
+     * Set the main-dex list output data.
      */
-    public Builder setMainDexListData(byte[] content) {
-      mainDexList = content == null ? null : Resource.fromBytes(null, content);
+    public Builder setMainDexListOutputData(byte[] content) {
+      mainDexListOutput = content == null ? null : Resource.fromBytes(null, content);
       return this;
     }
 
@@ -617,7 +675,9 @@
           proguardMap,
           proguardSeeds,
           packageDistribution,
-          mainDexList);
+          mainDexListResources,
+          mainDexListClasses,
+          mainDexListOutput);
     }
 
     private void addProgramFile(Path file) throws IOException {
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 9b5e007..02dd7be 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -69,7 +69,6 @@
   public Path seedsFile;
   public boolean printMapping;
   public Path printMappingFile;
-  public boolean printMainDexList;
   public Path printMainDexListFile;
   public boolean ignoreMissingClasses = false;
   public boolean skipMinification = false;
diff --git a/src/main/java/com/android/tools/r8/utils/MainDexList.java b/src/main/java/com/android/tools/r8/utils/MainDexList.java
index b219314..826901a 100644
--- a/src/main/java/com/android/tools/r8/utils/MainDexList.java
+++ b/src/main/java/com/android/tools/r8/utils/MainDexList.java
@@ -28,6 +28,18 @@
     }
   }
 
+  public static DexType parse(String clazz, DexItemFactory itemFactory) {
+    if (!clazz.endsWith(CLASS_EXTENSION)) {
+      throw new CompilationError("Illegal main-dex-list entry '" + clazz + "'.");
+    }
+    String name = clazz.substring(0, clazz.length() - CLASS_EXTENSION.length());
+    if (name.contains("" + JAVA_PACKAGE_SEPARATOR)) {
+      throw new CompilationError("Illegal main-dex-list entry '" + clazz + "'.");
+    }
+    String descriptor = "L" + name + ";";
+    return itemFactory.createType(descriptor);
+  }
+
   public static Set<DexType> parse(InputStream input, DexItemFactory itemFactory) {
     Set<DexType> result = Sets.newIdentityHashSet();
     try {
@@ -35,15 +47,7 @@
           new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
       String line;
       while ((line = file.readLine()) != null) {
-        if (!line.endsWith(CLASS_EXTENSION)) {
-          throw new CompilationError("Illegal main-dex-list entry '" + line + "'.");
-        }
-        String name = line.substring(0, line.length() - CLASS_EXTENSION.length());
-        if (name.contains("" + JAVA_PACKAGE_SEPARATOR)) {
-          throw new CompilationError("Illegal main-dex-list entry '" + line + "'.");
-        }
-        String descriptor = "L" + name + ";";
-        result.add(itemFactory.createType(descriptor));
+        result.add(parse(line, itemFactory));
       }
     } catch (IOException e) {
       throw new CompilationError("Cannot load main-dex-list.");
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
index f8ca2a8..dfc20f9 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
@@ -6,43 +6,45 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.CompilationException;
 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.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 public class MainDexListOutputTest extends TestBase {
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
   @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());
+    thrown.expect(CompilationException.class);
+    Path mainDexListOutput = temp.getRoot().toPath().resolve("main-dex-output.txt");
+    R8Command command =
+        ToolHelper.prepareR8CommandBuilder(readClasses(HelloWorldMain.class))
+            .setMainDexListOutputPath(mainDexListOutput)
+            .build();
+    ToolHelper.runR8(command);
   }
 
   @Test
   public void testWithMainDex() throws Exception {
     Path mainDexRules = writeTextToTempFile(keepMainProguardConfiguration(HelloWorldMain.class));
+    Path mainDexListOutput = temp.getRoot().toPath().resolve("main-dex-output.txt");
     R8Command command =
         ToolHelper.prepareR8CommandBuilder(readClasses(HelloWorldMain.class))
             .addMainDexRules(mainDexRules)
+            .setMainDexListOutputPath(mainDexListOutput)
             .build();
-    Path mainDexList = temp.newFile().toPath();
-    ToolHelper.runR8(command,
-        options -> {
-          options.printMainDexList = true;
-          options.printMainDexListFile = mainDexList;
-        });
+    ToolHelper.runR8(command);
     // Main dex list with the single class.
     assertEquals(
         ImmutableList.of(HelloWorldMain.class.getTypeName().replace('.', '/') + ".class"),
-        FileUtils.readTextFile(mainDexList));
+        FileUtils.readTextFile(mainDexListOutput));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index 714792d..95878ea 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -5,6 +5,7 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -216,7 +217,9 @@
     FileUtils.writeTextFile(mainDexList, list);
     Set<DexType> types = MainDexList.parse(mainDexList, factory);
     for (String entry : list) {
-      assertTrue(types.contains(factory.createType("L" + entry.replace(".class", "") + ";")));
+      DexType type = factory.createType("L" + entry.replace(".class", "") + ";");
+      assertTrue(types.contains(type));
+      assertSame(type, MainDexList.parse(entry, factory));
     }
   }
 
@@ -312,7 +315,7 @@
           .addProgramFiles(input)
           .setOutputPath(out);
       if (mainLists.get(i) != null) {
-        builder.setMainDexListFile(mainLists.get(i));
+        builder.addMainDexListFiles(mainLists.get(i));
       }
       ToolHelper.runD8(builder.build());
     }
@@ -398,8 +401,16 @@
     }
   }
 
+  private enum MultiDexTestMode {
+    SINGLE_FILE,
+    MULTIPLE_FILES,
+    STRINGS,
+    FILES_AND_STRINGS
+  }
+
   private void doVerifyMainDexContains(
-      List<String> mainDex, Path app, boolean singleDexApp, boolean minimalMainDex)
+      List<String> mainDex, Path app, boolean singleDexApp, boolean minimalMainDex,
+      MultiDexTestMode testMode)
       throws IOException, CompilationException, ExecutionException, ProguardRuleParserException {
     AndroidApp originalApp = AndroidApp.fromProgramFiles(app);
     DexInspector originalInspector = new DexInspector(originalApp);
@@ -408,18 +419,57 @@
           originalInspector.clazz(clazz).isPresent());
     }
     Path outDir = temp.newFolder().toPath();
-    Path mainDexList = temp.newFile().toPath();
-    FileUtils.writeTextFile(mainDexList, ListUtils.map(mainDex, MainDexListTests::typeToEntry));
-    R8Command command =
+    R8Command.Builder builder =
         R8Command.builder()
             .addProgramFiles(app)
-            .setMainDexListFile(mainDexList)
             .setMinimalMainDex(minimalMainDex && mainDex.size() > 0)
             .setOutputPath(outDir)
             .setTreeShaking(false)
-            .setMinification(false)
-            .build();
-    ToolHelper.runR8(command);
+            .setMinification(false);
+
+    switch (testMode) {
+      case SINGLE_FILE:
+        Path mainDexList = temp.newFile().toPath();
+        FileUtils.writeTextFile(mainDexList, ListUtils.map(mainDex, MainDexListTests::typeToEntry));
+        builder.addMainDexListFiles(mainDexList);
+        break;
+      case MULTIPLE_FILES: {
+        // Partion the main dex list into several files.
+        List<List<String>> partitions = Lists.partition(mainDex, Math.max(mainDex.size() / 3, 1));
+        List<Path> mainDexListFiles = new ArrayList<>();
+        for (List<String> partition : partitions) {
+          Path partialMainDexList = temp.newFile().toPath();
+          FileUtils.writeTextFile(partialMainDexList,
+              ListUtils.map(partition, MainDexListTests::typeToEntry));
+          mainDexListFiles.add(partialMainDexList);
+        }
+        builder.addMainDexListFiles(mainDexListFiles);
+        break;
+      }
+      case STRINGS:
+        builder.addMainDexClasses(mainDex);
+        break;
+      case FILES_AND_STRINGS: {
+        // Partion the main dex list add some parts through files and the other parts using strings.
+        List<List<String>> partitions = Lists.partition(mainDex, Math.max(mainDex.size() / 3, 1));
+        List<Path> mainDexListFiles = new ArrayList<>();
+        for (int i = 0; i < partitions.size(); i++) {
+          List<String> partition = partitions.get(i);
+          if (i % 2 == 0) {
+            Path partialMainDexList = temp.newFile().toPath();
+            FileUtils.writeTextFile(partialMainDexList,
+                ListUtils.map(partition, MainDexListTests::typeToEntry));
+            mainDexListFiles.add(partialMainDexList);
+          } else {
+            builder.addMainDexClasses(mainDex);
+          }
+        }
+        builder.addMainDexListFiles(mainDexListFiles);
+        break;
+      }
+    }
+
+    ToolHelper.runR8(builder.build());
     if (!singleDexApp && !minimalMainDex) {
       assertTrue("Output run only produced one dex file.",
           1 < Files.list(outDir).filter(FileUtils::isDexFile).count());
@@ -438,8 +488,10 @@
 
   private void verifyMainDexContains(List<String> mainDex, Path app, boolean singleDexApp)
       throws Throwable {
-    doVerifyMainDexContains(mainDex, app, singleDexApp, false);
-    doVerifyMainDexContains(mainDex, app, singleDexApp, true);
+    for (MultiDexTestMode multiDexTestMode : MultiDexTestMode.values()) {
+      doVerifyMainDexContains(mainDex, app, singleDexApp, false, multiDexTestMode);
+      doVerifyMainDexContains(mainDex, app, singleDexApp, true, multiDexTestMode);
+    }
   }
 
   public static AndroidApp generateApplication(List<String> classes, int minApi, int methodCount)
diff --git a/src/test/java/com/android/tools/r8/utils/D8CommandTest.java b/src/test/java/com/android/tools/r8/utils/D8CommandTest.java
index 443e257..9cc7eda 100644
--- a/src/test/java/com/android/tools/r8/utils/D8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/utils/D8CommandTest.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.google.common.collect.ImmutableList;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -157,32 +158,37 @@
 
   @Test
   public void mainDexList() throws Throwable {
-    Path mailDexList = temp.getRoot().toPath().resolve("main-dex-list.txt");
-    D8Command command = parse("--main-dex-list", mailDexList.toString());
+    Path mainDexList1 = temp.newFile("main-dex-list-1.txt").toPath();
+    Path mainDexList2 = temp.newFile("main-dex-list-2.txt").toPath();
+
+    D8Command command = parse("--main-dex-list", mainDexList1.toString());
+    assertTrue(ToolHelper.getApp(command).hasMainDexList());
+
+    command = parse(
+        "--main-dex-list", mainDexList1.toString(), "--main-dex-list", mainDexList2.toString());
     assertTrue(ToolHelper.getApp(command).hasMainDexList());
   }
 
   @Test
-  public void multipleMainDexList() throws Throwable {
-    thrown.expect(CompilationException.class);
-    Path mailDexList1 = temp.getRoot().toPath().resolve("main-dex-list-1.txt");
-    Path mailDexList2 = temp.getRoot().toPath().resolve("main-dex-list-2.txt");
-    parse("--main-dex-list", mailDexList1.toString(), "--main-dex-list", mailDexList2.toString());
+  public void nonExistingMainDexList() throws Throwable {
+    thrown.expect(FileNotFoundException.class);
+    Path mainDexList = temp.getRoot().toPath().resolve("main-dex-list.txt");
+    parse("--main-dex-list", mainDexList.toString());
   }
 
   @Test
   public void mainDexListWithFilePerClass() throws Throwable {
     thrown.expect(CompilationException.class);
-    Path mailDexList = temp.getRoot().toPath().resolve("main-dex-list.txt");
-    D8Command command = parse("--main-dex-list", mailDexList.toString(), "--file-per-class");
+    Path mainDexList = temp.newFile("main-dex-list.txt").toPath();
+    D8Command command = parse("--main-dex-list", mainDexList.toString(), "--file-per-class");
     assertTrue(ToolHelper.getApp(command).hasMainDexList());
   }
 
   @Test
   public void mainDexListWithIntermediate() throws Throwable {
     thrown.expect(CompilationException.class);
-    Path mailDexList = temp.getRoot().toPath().resolve("main-dex-list.txt");
-    D8Command command = parse("--main-dex-list", mailDexList.toString(), "--intermediate");
+    Path mainDexList = temp.newFile("main-dex-list.txt").toPath();
+    D8Command command = parse("--main-dex-list", mainDexList.toString(), "--intermediate");
     assertTrue(ToolHelper.getApp(command).hasMainDexList());
   }
 
diff --git a/src/test/java/com/android/tools/r8/utils/R8CommandTest.java b/src/test/java/com/android/tools/r8/utils/R8CommandTest.java
index a1e57ac..cb2085f 100644
--- a/src/test/java/com/android/tools/r8/utils/R8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/utils/R8CommandTest.java
@@ -16,8 +16,10 @@
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.google.common.collect.ImmutableList;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
@@ -90,8 +92,32 @@
 
   @Test
   public void mainDexRules() throws Throwable {
-    Path mailDexRules = temp.newFile("main-dex.rules").toPath();
-    parse("--main-dex-rules", mailDexRules.toString());
+    Path mainDexRules1 = temp.newFile("main-dex-1.rules").toPath();
+    Path mainDexRules2 = temp.newFile("main-dex-2.rules").toPath();
+    parse("--main-dex-rules", mainDexRules1.toString());
+    parse("--main-dex-rules", mainDexRules1.toString(), "--main-dex-rules", mainDexRules2.toString());
+  }
+
+  @Test
+  public void nonExistingMainDexRules() throws Throwable {
+    thrown.expect(NoSuchFileException.class);
+    Path mainDexRules = temp.getRoot().toPath().resolve("main-dex.rules");
+    parse("--main-dex-rules", mainDexRules.toString());
+  }
+
+  @Test
+  public void mainDexList() throws Throwable {
+    Path mainDexList1 = temp.newFile("main-dex-list-1.txt").toPath();
+    Path mainDexList2 = temp.newFile("main-dex-list-2.txt").toPath();
+    parse("--main-dex-list", mainDexList1.toString());
+    parse("--main-dex-list", mainDexList1.toString(), "--main-dex-list", mainDexList2.toString());
+  }
+
+  @Test
+  public void nonExistingMainDexList() throws Throwable {
+    thrown.expect(FileNotFoundException.class);
+    Path mainDexList = temp.getRoot().toPath().resolve("main-dex-list.txt");
+    parse("--main-dex-list", mainDexList.toString());
   }
 
   @Test
@@ -101,9 +127,27 @@
   }
 
   @Test
+  public void mainDexListOutput() throws Throwable {
+    Path mainDexRules = temp.newFile("main-dex.rules").toPath();
+    Path mainDexList = temp.newFile("main-dex-list.txt").toPath();
+    Path mainDexListOutput = temp.newFile("main-dex-out.txt").toPath();
+    parse("--main-dex-rules", mainDexRules.toString(),
+        "--main-dex-list-output", mainDexListOutput.toString());
+    parse("--main-dex-list", mainDexList.toString(),
+        "--main-dex-list-output", mainDexListOutput.toString());
+  }
+
+  @Test
+  public void mainDexListOutputWithoutAnyMainDexSpecification() throws Throwable {
+    thrown.expect(CompilationException.class);
+    Path mainDexListOutput = temp.newFile("main-dex-out.txt").toPath();
+    parse("--main-dex-list-output", mainDexListOutput.toString());
+  }
+
+  @Test
   public void mainDexRulesWithMinimalMainDex() throws Throwable {
-    Path mailDexRules = temp.newFile("main-dex.rules").toPath();
-    parse("--main-dex-rules", mailDexRules.toString(), "--minimal-main-dex");
+    Path mainDexRules = temp.newFile("main-dex.rules").toPath();
+    parse("--main-dex-rules", mainDexRules.toString(), "--minimal-main-dex");
   }
 
   @Test