Merge "Added support for minimal main dex"
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 34834f2..06ebe60 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -26,6 +26,7 @@
   public static class Builder extends BaseCommand.Builder<R8Command, Builder> {
 
     private final List<Path> mainDexRules = new ArrayList<>();
+    private boolean minimalMainDex = false;
     private final List<Path> proguardConfigFiles = new ArrayList<>();
     private Optional<Boolean> treeShaking = Optional.empty();
     private Optional<Boolean> minification = Optional.empty();
@@ -77,6 +78,16 @@
     }
 
     /**
+     * Request minimal main dex generated when main dex rules are used.
+     *
+     * The main purpose of this is to verify that the main dex rules are sufficient
+     * for running on a platform without native multi dex support.
+     */
+    public Builder setMinimalMainDex(boolean value) {
+      minimalMainDex = value;
+      return this;
+    }
+    /**
      * Add proguard configuration file resources.
      */
     public Builder addProguardConfigurationFiles(Path... paths) {
@@ -161,6 +172,7 @@
           getOutputPath(),
           getOutputMode(),
           mainDexKeepRules,
+          minimalMainDex,
           configuration,
           getMode(),
           getMinApiLevel(),
@@ -196,6 +208,7 @@
       "  --help                  # Print this message."));
 
   private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
+  private final boolean minimalMainDex;
   private final ProguardConfiguration proguardConfiguration;
   private final boolean useTreeShaking;
   private final boolean useMinification;
@@ -259,6 +272,8 @@
         builder.setMinification(false);
       } else if (arg.equals("--multidex-rules")) {
         builder.addMainDexRules(Paths.get(args[++i]));
+      } else if (arg.equals("--minimal-maindex")) {
+        builder.setMinimalMainDex(true);
       } else if (arg.equals("--pg-conf")) {
         builder.addProguardConfigurationFiles(Paths.get(args[++i]));
       } else if (arg.equals("--pg-map")) {
@@ -300,6 +315,7 @@
       Path outputPath,
       OutputMode outputMode,
       ImmutableList<ProguardConfigurationRule> mainDexKeepRules,
+      boolean minimalMainDex,
       ProguardConfiguration proguardConfiguration,
       CompilationMode mode,
       int minApiLevel,
@@ -311,6 +327,7 @@
     assert mainDexKeepRules != null;
     assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
     this.mainDexKeepRules = mainDexKeepRules;
+    this.minimalMainDex = minimalMainDex;
     this.proguardConfiguration = proguardConfiguration;
     this.useTreeShaking = useTreeShaking;
     this.useMinification = useMinification;
@@ -320,6 +337,7 @@
   private R8Command(boolean printHelp, boolean printVersion) {
     super(printHelp, printVersion);
     mainDexKeepRules = ImmutableList.of();
+    minimalMainDex = false;
     proguardConfiguration = null;
     useTreeShaking = false;
     useMinification = false;
@@ -376,6 +394,7 @@
     internal.classObfuscationDictionary = proguardConfiguration.getClassObfuscationDictionary();
     internal.obfuscationDictionary = proguardConfiguration.getObfuscationDictionary();
     internal.mainDexKeepRules = mainDexKeepRules;
+    internal.minimalMainDex = minimalMainDex;
     internal.keepRules = proguardConfiguration.getRules();
     internal.dontWarnPatterns = proguardConfiguration.getDontWarnPatterns();
     internal.outputMode = getOutputMode();
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 7476792..d7fb9f8 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -125,16 +125,20 @@
             "Cannot combine package distribution definition with file-per-class option.";
         distributor = new FilePerClassDistributor(this);
       } else if (packageDistribution != null) {
+        assert !options.minimalMainDex :
+            "Cannot combine package distribution definition with minimal-main-dex option.";
         distributor = new PackageMapDistributor(this, packageDistribution, executorService);
       } else {
-        distributor = new FillFilesDistributor(this);
+        distributor = new FillFilesDistributor(this, options.minimalMainDex);
       }
       Map<Integer, VirtualFile> newFiles = distributor.run();
 
-      // Write the dex files and the Proguard mapping file in parallel.
+      // Write the dex files and the Proguard mapping file in parallel. Use a linked hash map
+      // as the order matters when addDexProgramData is called below.
       LinkedHashMap<VirtualFile, Future<byte[]>> dexDataFutures = new LinkedHashMap<>();
-      for (Integer index : newFiles.keySet()) {
-        VirtualFile newFile = newFiles.get(index);
+      for (int i = 0; i < newFiles.size(); i++) {
+        VirtualFile newFile = newFiles.get(i);
+        assert newFile.getId() == i;
         if (!newFile.isEmpty()) {
           dexDataFutures.put(newFile, executorService.submit(() -> writeDexFile(newFile)));
         }
diff --git a/src/main/java/com/android/tools/r8/dex/Constants.java b/src/main/java/com/android/tools/r8/dex/Constants.java
index 201fc82..9527a97 100644
--- a/src/main/java/com/android/tools/r8/dex/Constants.java
+++ b/src/main/java/com/android/tools/r8/dex/Constants.java
@@ -11,6 +11,7 @@
   public static final int ANDROID_O_API = 26;
   public static final int ANDROID_N_API = 24;
   public static final int ANDROID_K_API = 19;
+  public static final int ANDROID_L_API = 21;
   public static final int DEFAULT_ANDROID_API = 1;
 
   /** dex file version number for Android O (API level 26) */
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 3db0502..55511fd 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.utils.PackageDistribution;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.google.common.collect.Iterators;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
@@ -53,9 +54,17 @@
 
 public class VirtualFile {
 
+  // The fill strategy determine how to distribute classes into dex files.
   enum FillStrategy {
+    // Only put classes matches by the main dex rules into the first dex file. Distribute remaining
+    // classes in additional dex files filling each dex file as much as possible.
+    MINIMAL_MAIN_DEX,
+    // Distribute classes in as few dex files as possible filling each dex file as much as possible.
     FILL_MAX,
+    // Distribute classes keeping some space for future growth. This is mainly useful together with
+    // the package map distribution.
     LEAVE_SPACE_FOR_GROWTH,
+    // TODO(sgjesse): Does "minimal main dex" combined with "leave space for growth" make sense?
   }
 
   private static final int MAX_ENTRIES = (Short.MAX_VALUE << 1) + 1;
@@ -75,6 +84,10 @@
     this.transaction = new IndexedItemTransaction(indexedItems, namingLens);
   }
 
+  public int getId() {
+    return id;
+  }
+
   public Set<String> getClassDescriptors() {
     Set<String> classDescriptors = new HashSet<>();
     for (DexProgramClass clazz : indexedItems.classes) {
@@ -187,7 +200,7 @@
   public abstract static class Distributor {
     protected final DexApplication application;
     protected final ApplicationWriter writer;
-    protected final Map<Integer, VirtualFile> nameToFileMap = new LinkedHashMap<>();
+    protected final Map<Integer, VirtualFile> nameToFileMap = new HashMap<>();
 
     public Distributor(ApplicationWriter writer) {
       this.application = writer.application;
@@ -283,8 +296,11 @@
   }
 
   public static class FillFilesDistributor extends DistributorBase {
-    public FillFilesDistributor(ApplicationWriter writer) {
+    private final FillStrategy fillStrategy;
+
+    public FillFilesDistributor(ApplicationWriter writer, boolean minimalMainDex) {
       super(writer);
+      this.fillStrategy = minimalMainDex ? FillStrategy.MINIMAL_MAIN_DEX : FillStrategy.FILL_MAX;
     }
 
     public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
@@ -297,13 +313,13 @@
       // First fill required classes into the main dex file.
       fillForMainDexList(classes);
 
-      // Sort the classes based on the original names.
+      // Sort the remaining classes based on the original names.
       // This with make classes from the same package be adjacent.
       classes = sortClassesByPackage(classes, originalNames);
 
       new PackageSplitPopulator(
           nameToFileMap, classes, originalNames, null, application.dexItemFactory,
-          FillStrategy.FILL_MAX, writer.namingLens)
+          fillStrategy, writer.namingLens)
           .call();
       return nameToFileMap;
     }
@@ -336,7 +352,7 @@
       // First fill required classes into the main dex file.
       fillForMainDexList(classes);
 
-      // Sort the classes based on the original names.
+      // Sort the remaining classes based on the original names.
       // This with make classes from the same package be adjacent.
       classes = sortClassesByPackage(classes, originalNames);
 
@@ -712,6 +728,74 @@
   }
 
   /**
+   * Helper class to cycle through the set of virtual files.
+   *
+   * Iteration starts at the first file and iterates through all files.
+   *
+   * When {@link VirtualFileCycler#restart()} is called iteration of all files is restarted at the
+   * current file.
+   *
+   * If the fill strategy indicate that the main dex file should be minimal, then the main dex file
+   * will not be part of the iteration.
+   */
+  private static class VirtualFileCycler {
+    private Map<Integer, VirtualFile> files;
+    private final NamingLens namingLens;
+    private final FillStrategy fillStrategy;
+
+    private int nextFileId;
+    private Iterator<VirtualFile> allFilesCyclic;
+    private Iterator<VirtualFile> activeFiles;
+
+    VirtualFileCycler(Map<Integer, VirtualFile> files, NamingLens namingLens,
+        FillStrategy fillStrategy) {
+      this.files = files;
+      this.namingLens = namingLens;
+      this.fillStrategy = fillStrategy;
+
+      nextFileId = files.size();
+      if (fillStrategy == FillStrategy.MINIMAL_MAIN_DEX) {
+        // The main dex file is filtered out, so ensure at least one file for the remaining
+        // classes
+        files.put(nextFileId, new VirtualFile(nextFileId, namingLens));
+        this.files = Maps.filterKeys(files, key -> key != 0);
+        nextFileId++;
+      }
+
+      reset();
+    }
+
+    private void reset() {
+      allFilesCyclic = Iterators.cycle(files.values());
+      restart();
+    }
+
+    boolean hasNext() {
+      return activeFiles.hasNext();
+    }
+
+    VirtualFile next() {
+      VirtualFile next = activeFiles.next();
+      assert fillStrategy != FillStrategy.MINIMAL_MAIN_DEX || next.getId() != 0;
+      return next;
+    }
+
+    // Start a new iteration over all files, starting at the current one.
+    void restart() {
+      activeFiles = Iterators.limit(allFilesCyclic, files.size());
+    }
+
+    VirtualFile addFile() {
+      VirtualFile newFile = new VirtualFile(nextFileId, namingLens);
+      files.put(nextFileId, newFile);
+      nextFileId++;
+
+      reset();
+      return newFile;
+    }
+  }
+
+  /**
    * Distributes the given classes over the files in package order.
    *
    * <p>The populator avoids package splits. Big packages are split into subpackages if their size
@@ -735,13 +819,12 @@
      */
     private static final int MIN_FILL_FACTOR = 5;
 
-    private final Map<Integer, VirtualFile> files;
     private final List<DexProgramClass> classes;
     private final Map<DexProgramClass, String> originalNames;
     private final Set<String> previousPrefixes;
     private final DexItemFactory dexItemFactory;
     private final FillStrategy fillStrategy;
-    private final NamingLens namingLens;
+    private final VirtualFileCycler cycler;
 
     PackageSplitPopulator(
         Map<Integer, VirtualFile> files,
@@ -751,13 +834,12 @@
         DexItemFactory dexItemFactory,
         FillStrategy fillStrategy,
         NamingLens namingLens) {
-      this.files = files;
       this.classes = new ArrayList<>(classes);
       this.originalNames = originalNames;
       this.previousPrefixes = previousPrefixes;
       this.dexItemFactory = dexItemFactory;
       this.fillStrategy = fillStrategy;
-      this.namingLens = namingLens;
+      this.cycler = new VirtualFileCycler(files, namingLens, fillStrategy);
     }
 
     private String getOriginalName(DexProgramClass clazz) {
@@ -766,14 +848,12 @@
 
     @Override
     public Map<String, Integer> call() throws IOException {
-      Iterator<VirtualFile> allFilesCyclic = Iterators.cycle(files.values());
-      Iterator<VirtualFile> activeFiles = Iterators.limit(allFilesCyclic, files.size());
       int prefixLength = MINIMUM_PREFIX_LENGTH;
       int transactionStartIndex = 0;
       int fileStartIndex = 0;
       String currentPrefix = null;
       Map<String, Integer> newPackageAssignments = new LinkedHashMap<>();
-      VirtualFile current = activeFiles.next();
+      VirtualFile current = cycler.next();
       List<DexProgramClass> nonPackageClasses = new ArrayList<>();
       for (int classIndex = 0; classIndex < classes.size(); classIndex++) {
         DexProgramClass clazz = classes.get(classIndex);
@@ -781,8 +861,8 @@
         if (!PackageMapPopulator.coveredByPrefix(originalName, currentPrefix)) {
           if (currentPrefix != null) {
             current.commitTransaction();
-            // Reset the iterator to again iterate over all files, starting with the current one.
-            activeFiles = Iterators.limit(allFilesCyclic, files.size());
+            // Reset the cycler to again iterate over all files, starting with the current one.
+            cycler.restart();
             assert !newPackageAssignments.containsKey(currentPrefix);
             newPackageAssignments.put(currentPrefix, current.id);
             // Try to reduce the prefix length if possible. Only do this on a successful commit.
@@ -832,7 +912,7 @@
             // The idea is that we do not increase the number of files, so it has to fit
             // somewhere.
             fileStartIndex = transactionStartIndex;
-            if (!activeFiles.hasNext()) {
+            if (!cycler.hasNext()) {
               // Special case where we simply will never be able to fit the current package into
               // one dex file. This is currently the case for Strings in jumbo tests, see:
               // b/33227518
@@ -843,11 +923,9 @@
                 transactionStartIndex = classIndex + 1;
               }
               // All files are filled up to the 20% mark.
-              files.put(files.size(), new VirtualFile(files.size(), namingLens));
-              allFilesCyclic = Iterators.cycle(files.values());
-              activeFiles = Iterators.limit(allFilesCyclic, files.size());
+              cycler.addFile();
             }
-            current = activeFiles.next();
+            current = cycler.next();
           }
           currentPrefix = null;
           // Go back to previous start index.
@@ -861,24 +939,25 @@
         newPackageAssignments.put(currentPrefix, current.id);
       }
       if (nonPackageClasses.size() > 0) {
-        addNonPackageClasses(Iterators.limit(allFilesCyclic, files.size()), nonPackageClasses);
+        addNonPackageClasses(cycler, nonPackageClasses);
       }
       return newPackageAssignments;
     }
 
-    private void addNonPackageClasses(Iterator<VirtualFile> activeFiles,
-        List<DexProgramClass> nonPackageClasses) {
+    private void addNonPackageClasses(
+        VirtualFileCycler cycler, List<DexProgramClass> nonPackageClasses) {
+      cycler.restart();
       VirtualFile current;
-      current = activeFiles.next();
+      current = cycler.next();
       for (DexProgramClass clazz : nonPackageClasses) {
         if (current.isFilledEnough(fillStrategy)) {
-          current = getVirtualFile(activeFiles);
+          current = getVirtualFile(cycler);
         }
         current.addClass(clazz);
         while (current.isFull()) {
           // This only happens if we have a huge class, that takes up more than 20% of a dex file.
           current.abortTransaction();
-          current = getVirtualFile(activeFiles);
+          current = getVirtualFile(cycler);
           boolean wasEmpty = current.isEmpty();
           current.addClass(clazz);
           if (wasEmpty && current.isFull()) {
@@ -890,13 +969,12 @@
       }
     }
 
-    private VirtualFile getVirtualFile(Iterator<VirtualFile> activeFiles) {
+    private VirtualFile getVirtualFile(VirtualFileCycler cycler) {
       VirtualFile current = null;
-      while (activeFiles.hasNext()
-          && (current = activeFiles.next()).isFilledEnough(fillStrategy)) {}
+      while (cycler.hasNext()
+          && (current = cycler.next()).isFilledEnough(fillStrategy)) {}
       if (current == null || current.isFilledEnough(fillStrategy)) {
-        current = new VirtualFile(files.size(), namingLens);
-        files.put(files.size(), current);
+        current = cycler.addFile();
       }
       return current;
     }
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 9d2379a..7b1cfe1 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -86,6 +86,7 @@
   public List<String> obfuscationDictionary = ImmutableList.of();
 
   public ImmutableList<ProguardConfigurationRule> mainDexKeepRules = ImmutableList.of();
+  public boolean minimalMainDex;
   public ImmutableList<ProguardConfigurationRule> keepRules = ImmutableList.of();
   public ImmutableSet<ProguardTypeMatcher> dontWarnPatterns = ImmutableSet.of();
 
diff --git a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
index 4131b86..bd97a3b 100644
--- a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
+++ b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
@@ -8,11 +8,15 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.maindexlist.MainDexListTests;
+import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.StringUtils.BraceType;
+import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -21,6 +25,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
@@ -95,9 +100,14 @@
   }
 
   @Test
-  public void singleDexProgramIsTooLarge() throws IOException {
+  public void singleDexProgramIsTooLarge() throws IOException, ExecutionException {
+    // Generate an application that will not fit into a single dex file.
+    AndroidApp generated = MainDexListTests.generateApplication(
+        ImmutableList.of("A", "B"), Constants.DEFAULT_ANDROID_API, Constants.U16BIT_MAX / 2 + 1);
+    Path applicationJar = temp.newFile("application.jar").toPath();
+    generated.write(applicationJar, OutputMode.Indexed, true);
     thrown.expect(CompilationError.class);
-    runDexer(MainDexListTests.getTwoLargeClassesAppPath().toString());
+    runDexer(applicationJar.toString());
   }
 
   @Test
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 f540bc0..4eb8545 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -3,17 +3,15 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.maindexlist;
 
-import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.CompilationException;
-import com.android.tools.r8.D8;
 import com.android.tools.r8.D8Command;
 import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.dex.ApplicationWriter;
 import com.android.tools.r8.dex.Constants;
@@ -47,6 +45,7 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
@@ -56,123 +55,100 @@
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-import com.google.common.io.ByteStreams;
-import com.google.common.io.Closer;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
 
-public class MainDexListTests {
-
-  private static final Path BASE =
-      Paths.get("src", "test", "java", "com", "android", "tools", "r8", "maindexlist");
-
-  private static boolean verifyApplications = true;
-  private static boolean regenerateApplications = false;
+public class MainDexListTests extends TestBase {
 
   private static final int MAX_METHOD_COUNT = Constants.U16BIT_MAX;
+
   private static final List<String> TWO_LARGE_CLASSES = ImmutableList.of("A", "B");
-  private static final String TWO_LARGE_CLASSES_APP = "two-large-classes.zip";
+  private static final int MANY_CLASSES_COUNT = 10000;
+  private static final int MANY_CLASSES_SINGLE_DEX_METHODS_PER_CLASS = 2;
+  private static final int MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS = 10;
+  private static List<String> MANY_CLASSES;
 
-  private static final int MANY_CLASSES_COUNT = 1000;
-  private static final List<String> MANY_CLASSES;
-  private static final String MANY_CLASSES_APP = "many-classes.zip";
+  @ClassRule
+  public static TemporaryFolder generatedApplicationsFolder = new TemporaryFolder();
 
-  static {
+  // Generate the test applications in a @BeforeClass method, as they are used by several tests.
+  @BeforeClass
+  public static void generateTestApplications() throws Throwable {
     ImmutableList.Builder<String> builder = ImmutableList.builder();
     for (int i = 0; i < MANY_CLASSES_COUNT; ++i) {
       String pkg = i % 2 == 0 ? "a" : "b";
       builder.add(pkg + ".Class" + i);
     }
     MANY_CLASSES = builder.build();
+
+    // Generates an application with many classes, every even in one package and every odd in
+    // another. Keep the number of methods low enough for single dex application.
+    AndroidApp generated = generateApplication(
+        MANY_CLASSES, Constants.DEFAULT_ANDROID_API, MANY_CLASSES_SINGLE_DEX_METHODS_PER_CLASS);
+    generated.write(getManyClassesSingleDexAppPath(), OutputMode.Indexed, false);
+
+    // Generates an application with many classes, every even in one package and every odd in
+    // another. Add enough methods so the application cannot fit into one dex file.
+    generated = generateApplication(
+        MANY_CLASSES, Constants.ANDROID_L_API, MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS);
+    generated.write(getManyClassesMultiDexAppPath(), OutputMode.Indexed, false);
+
+    // Generates an application with two classes, each with the maximum possible number of methods.
+    generated = generateApplication(TWO_LARGE_CLASSES, Constants.ANDROID_N_API, MAX_METHOD_COUNT);
+    generated.write(getTwoLargeClassesAppPath(), OutputMode.Indexed, false);
   }
 
-  public static Path getTwoLargeClassesAppPath() {
-    return BASE.resolve(TWO_LARGE_CLASSES_APP);
+  private static Path getTwoLargeClassesAppPath() {
+    return generatedApplicationsFolder.getRoot().toPath().resolve("two-large-classes.zip");
   }
 
-  public static Path getManyClassesAppPath() {
-    return BASE.resolve(MANY_CLASSES_APP);
+  private static Path getManyClassesSingleDexAppPath() {
+    return generatedApplicationsFolder.getRoot().toPath().resolve("many-classes-mono.zip");
   }
 
-  // Generates an application with two classes, each with the maximum possible number of methods.
-  @Test
-  public void generateTwoLargeClasses() throws IOException, ExecutionException {
-    if (!verifyApplications && !regenerateApplications) {
-      return;
-    }
-    AndroidApp generated = generateApplication(TWO_LARGE_CLASSES, MAX_METHOD_COUNT);
-    if (regenerateApplications) {
-      generated.write(getTwoLargeClassesAppPath(), OutputMode.Indexed, true);
-    } else {
-      AndroidApp cached = AndroidApp.fromProgramFiles(getTwoLargeClassesAppPath());
-      compareToCachedVersion(cached, generated, TWO_LARGE_CLASSES_APP);
-    }
+  private static Path getManyClassesMultiDexAppPath() {
+    return generatedApplicationsFolder.getRoot().toPath().resolve("many-classes-stereo.zip");
   }
 
-  // Generates an application with many classes, every even in one package and every odd in another.
-  @Test
-  public void generateManyClasses() throws IOException, ExecutionException {
-    if (!verifyApplications && !regenerateApplications) {
-      return;
-    }
-    AndroidApp generated = generateApplication(MANY_CLASSES, 1);
-    if (regenerateApplications) {
-      generated.write(getManyClassesAppPath(), OutputMode.Indexed, true);
-    } else {
-      AndroidApp cached = AndroidApp.fromProgramFiles(getManyClassesAppPath());
-      compareToCachedVersion(cached, generated, MANY_CLASSES_APP);
-    }
-  }
-
-  private static void compareToCachedVersion(AndroidApp cached, AndroidApp generated, String name)
-      throws IOException {
-    assertEquals("On-disk cached app (" + name + ") differs in file count from regeneration"
-            + "Set 'regenerateApplications = true' and rerun this test to update the cache.",
-        cached.getDexProgramResources().size(), generated.getDexProgramResources().size());
-    try (Closer closer = Closer.create()) {
-      for (int i = 0; i < cached.getDexProgramResources().size(); i++) {
-        byte[] cachedBytes =
-            ByteStreams.toByteArray(cached.getDexProgramResources().get(i).getStream(closer));
-        byte[] generatedBytes =
-            ByteStreams.toByteArray(generated.getDexProgramResources().get(i).getStream(closer));
-        assertArrayEquals("On-disk cached app differs in byte content from regeneration"
-                + "Set 'regenerateApplications = true' and rerun this test to update the cache.",
-            cachedBytes, generatedBytes);
-      }
-    }
-  }
-
-  @Rule
-  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
-
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
   @Test
+  public void checkGeneratedFileFitInSingleDexFile() {
+    assertTrue(MANY_CLASSES_COUNT * MANY_CLASSES_SINGLE_DEX_METHODS_PER_CLASS <= MAX_METHOD_COUNT);
+  }
+
+  @Test
+  public void checkGeneratedFileNeedsTwoDexFiles() {
+    assertTrue(MANY_CLASSES_COUNT * MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS > MAX_METHOD_COUNT);
+  }
+
+  @Test
   public void putFirstClassInMainDexList() throws Throwable {
-    verifyMainDexContains(TWO_LARGE_CLASSES.subList(0, 1), getTwoLargeClassesAppPath());
+    verifyMainDexContains(TWO_LARGE_CLASSES.subList(0, 1), getTwoLargeClassesAppPath(), false);
   }
 
   @Test
   public void putSecondClassInMainDexList() throws Throwable {
-    verifyMainDexContains(TWO_LARGE_CLASSES.subList(1, 2), getTwoLargeClassesAppPath());
+    verifyMainDexContains(TWO_LARGE_CLASSES.subList(1, 2), getTwoLargeClassesAppPath(), false);
   }
 
   @Test
   public void cannotFitBothIntoMainDex() throws Throwable {
     thrown.expect(CompilationError.class);
-    verifyMainDexContains(TWO_LARGE_CLASSES, getTwoLargeClassesAppPath());
+    verifyMainDexContains(TWO_LARGE_CLASSES, getTwoLargeClassesAppPath(), false);
   }
 
   @Test
@@ -184,7 +160,28 @@
         mainDexBuilder.add(clazz);
       }
     }
-    verifyMainDexContains(mainDexBuilder.build(), getManyClassesAppPath());
+    verifyMainDexContains(mainDexBuilder.build(), getManyClassesSingleDexAppPath(), true);
+    verifyMainDexContains(mainDexBuilder.build(), getManyClassesMultiDexAppPath(), false);
+  }
+
+  @Test
+  public void singleClassInMainDex() throws Throwable {
+    ImmutableList<String> mainDex = ImmutableList.of(MANY_CLASSES.get(0));
+    verifyMainDexContains(mainDex, getManyClassesSingleDexAppPath(), true);
+    verifyMainDexContains(mainDex, getManyClassesMultiDexAppPath(), false);
+  }
+
+  @Test
+  public void allClassesInMainDex() throws Throwable {
+    // Degenerated case with an app thats fit into a single dex, and where the main dex list
+    // contains all classes.
+    verifyMainDexContains(MANY_CLASSES, getManyClassesSingleDexAppPath(), true);
+  }
+
+  @Test
+  public void cannotFitAllIntoMainDex() throws Throwable {
+    thrown.expect(CompilationError.class);
+    verifyMainDexContains(MANY_CLASSES, getManyClassesMultiDexAppPath(), false);
   }
 
   @Test
@@ -320,10 +317,39 @@
   }
 
   private static String typeToEntry(String type) {
-    return type.replace(".", "/") + CLASS_EXTENSION;
+    return type.replace(".", "/") + FileUtils.CLASS_EXTENSION;
   }
 
-  private void verifyMainDexContains(List<String> mainDex, Path app)
+  private void failedToFindClassInExpectedFile(Path outDir, String clazz) throws IOException {
+    Files.list(outDir)
+        .filter(FileUtils::isDexFile)
+        .forEach(
+            p -> {
+              try {
+                DexInspector i = new DexInspector(AndroidApp.fromProgramFiles(p));
+                assertFalse("Found " + clazz + " in file " + p, i.clazz(clazz).isPresent());
+              } catch (IOException | ExecutionException e) {
+                e.printStackTrace();
+              }
+            });
+    fail("Failed to find class " + clazz + "in any file...");
+  }
+
+  private void assertMainDexClass(FoundClassSubject clazz, List<String> mainDex) {
+    if (!mainDex.contains(clazz.toString())) {
+      StringBuilder builder = new StringBuilder();
+      for (int i = 0; i < mainDex.size(); i++) {
+        builder.append(i == 0 ? "[" : ", ");
+        builder.append(mainDex.get(i));
+      }
+      builder.append("]");
+      fail("Class " + clazz + " found in main dex, " +
+          "only expected explicit main dex classes " + builder +" in main dex file");
+    }
+  }
+
+  private void doVerifyMainDexContains(
+      List<String> mainDex, Path app, boolean singleDexApp, boolean minimalMainDex)
       throws IOException, CompilationException, ExecutionException, ProguardRuleParserException {
     AndroidApp originalApp = AndroidApp.fromProgramFiles(app);
     DexInspector originalInspector = new DexInspector(originalApp);
@@ -331,44 +357,42 @@
       assertTrue("Class " + clazz + " does not exist in input",
           originalInspector.clazz(clazz).isPresent());
     }
-    Path outDir = temp.newFolder("out").toPath();
-    Path mainDexList = temp.getRoot().toPath().resolve("main-dex-list.txt");
+    Path outDir = temp.newFolder().toPath();
+    Path mainDexList = temp.newFile().toPath();
     FileUtils.writeTextFile(mainDexList, ListUtils.map(mainDex, MainDexListTests::typeToEntry));
-    Path packageDistribution = temp.getRoot().toPath().resolve("package.map");
-    FileUtils.writeTextFile(packageDistribution, ImmutableList.of("a.*:2", "b.*:1"));
     R8Command command =
         R8Command.builder()
             .addProgramFiles(app)
-            .setPackageDistributionFile(packageDistribution)
             .setMainDexListFile(mainDexList)
+            .setMinimalMainDex(minimalMainDex)
             .setOutputPath(outDir)
             .setTreeShaking(false)
             .setMinification(false)
             .build();
     ToolHelper.runR8(command);
-    assertTrue("Output run only produced one dex file. Invalid test",
-        1 < Files.list(outDir).filter(FileUtils::isDexFile).count());
+    if (!singleDexApp && !minimalMainDex) {
+      assertTrue("Output run only produced one dex file.",
+          1 < Files.list(outDir).filter(FileUtils::isDexFile).count());
+    }
     DexInspector inspector =
         new DexInspector(AndroidApp.fromProgramFiles(outDir.resolve("classes.dex")));
     for (String clazz : mainDex) {
       if (!inspector.clazz(clazz).isPresent()) {
-        Files.list(outDir)
-            .filter(FileUtils::isDexFile)
-            .forEach(
-                p -> {
-                  try {
-                    DexInspector i = new DexInspector(AndroidApp.fromProgramFiles(p));
-                    assertFalse("Found " + clazz + " in file " + p, i.clazz(clazz).isPresent());
-                  } catch (IOException | ExecutionException e) {
-                    e.printStackTrace();
-                  }
-                });
-        fail("Failed to find class " + clazz + "in any file...");
+        failedToFindClassInExpectedFile(outDir, clazz);
       }
     }
+    if (minimalMainDex) {
+      inspector.forAllClasses(clazz -> assertMainDexClass(clazz, mainDex));
+    }
   }
 
-  private static AndroidApp generateApplication(List<String> classes, int methodCount)
+  private void verifyMainDexContains(List<String> mainDex, Path app, boolean singleDexApp)
+      throws Throwable {
+    doVerifyMainDexContains(mainDex, app, singleDexApp, false);
+    doVerifyMainDexContains(mainDex, app, singleDexApp, true);
+  }
+
+  public static AndroidApp generateApplication(List<String> classes, int minApi, int methodCount)
       throws IOException, ExecutionException {
     Timing timing = new Timing("MainDexListTests");
     InternalOptions options = new InternalOptions();
diff --git a/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip b/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip
deleted file mode 100644
index fd4ecdc..0000000
--- a/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip
+++ /dev/null
Binary files differ
diff --git a/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip b/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip
deleted file mode 100644
index 9ad745f..0000000
--- a/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip
+++ /dev/null
Binary files differ