DRAFT: introducing --dex-per-class option in D8.

Proposal to better support incremental compilation: for
recompiling N changed/affected files instead of running D8
on each of them separately, we run D8 on all of them and
split the result into separate DEX files.

After transformation the mapping from resulting resource into
the class is available via Resource.getClassDescriptor().

When written into directly, all dex files are named based on
the original class names, such that class com.blah.Foo will
be written into DEX file named com.blah.Foo.dex.

BUG=

Change-Id: I359a6c6399ea786662f0c2a1f9c8d4cae416bcbd
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java
index bf58365..3497778 100644
--- a/src/main/java/com/android/tools/r8/BaseCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import java.io.IOException;
@@ -19,6 +20,7 @@
 
   private final AndroidApp app;
   private final Path outputPath;
+  private final OutputMode outputMode;
   private final CompilationMode mode;
   private final int minApiLevel;
 
@@ -28,16 +30,19 @@
     // All other fields are initialized with stub/invalid values.
     this.app = null;
     this.outputPath = null;
+    this.outputMode = OutputMode.Indexed;
     this.mode = null;
     this.minApiLevel = 0;
   }
 
-  BaseCommand(AndroidApp app, Path outputPath, CompilationMode mode, int minApiLevel) {
+  BaseCommand(AndroidApp app, Path outputPath,
+      OutputMode outputMode, CompilationMode mode, int minApiLevel) {
     assert app != null;
     assert mode != null;
     assert minApiLevel > 0;
     this.app = app;
     this.outputPath = outputPath;
+    this.outputMode = outputMode;
     this.mode = mode;
     this.minApiLevel = minApiLevel;
     // Print options are not set.
@@ -73,12 +78,17 @@
     return minApiLevel;
   }
 
+  public OutputMode getOutputMode() {
+    return outputMode;
+  }
+
   abstract static class Builder<C extends BaseCommand, B extends Builder<C, B>> {
 
     private boolean printHelp = false;
     private boolean printVersion = false;
     private final AndroidApp.Builder app;
     private Path outputPath = null;
+    private OutputMode outputMode = OutputMode.Indexed;
     private CompilationMode mode;
     private int minApiLevel = Constants.DEFAULT_ANDROID_API;
 
@@ -183,12 +193,23 @@
       return outputPath;
     }
 
+    /** Get the output mode. */
+    public OutputMode getOutputMode() {
+      return outputMode;
+    }
+
     /** Set an output path. Must be an existing directory or a non-existent zip file. */
     public B setOutputPath(Path outputPath) throws CompilationException {
       this.outputPath = FileUtils.validateOutputFile(outputPath);
       return self();
     }
 
+    /** Set an output mode. */
+    public B setOutputMode(OutputMode outputMode) {
+      this.outputMode = outputMode;
+      return self();
+    }
+
     /** Get the minimum API level (aka SDK version). */
     public int getMinApiLevel() {
       return minApiLevel;
diff --git a/src/main/java/com/android/tools/r8/BaseOutput.java b/src/main/java/com/android/tools/r8/BaseOutput.java
index 0f082c9..cb4d473 100644
--- a/src/main/java/com/android/tools/r8/BaseOutput.java
+++ b/src/main/java/com/android/tools/r8/BaseOutput.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.OutputMode;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -11,9 +12,11 @@
 abstract class BaseOutput {
 
   private final AndroidApp app;
+  private final OutputMode outputMode;
 
-  BaseOutput(AndroidApp app) {
+  BaseOutput(AndroidApp app, OutputMode outputMode) {
     this.app = app;
+    this.outputMode = outputMode;
   }
 
   // Internal access to the underlying app.
@@ -21,6 +24,11 @@
     return app;
   }
 
+  // Internal access to the options.
+  public OutputMode getOutputMode() {
+    return outputMode;
+  }
+
   /**
    * Get the list of compiled DEX resources.
    *
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 82831f3..0a0c976 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -59,8 +59,9 @@
    * @return the compilation result.
    */
   public static D8Output run(D8Command command) throws IOException {
-    D8Output output = new D8Output(
-        runForTesting(command.getInputApp(), command.getInternalOptions()).androidApp);
+    CompilationResult result = runForTesting(command.getInputApp(), command.getInternalOptions());
+    assert result != null;
+    D8Output output = new D8Output(result.androidApp, command.getOutputMode());
     if (command.getOutputPath() != null) {
       output.write(command.getOutputPath());
     }
@@ -78,8 +79,10 @@
    * @return the compilation result.
    */
   public static D8Output run(D8Command command, ExecutorService executor) throws IOException {
-    D8Output output = new D8Output(
-        runForTesting(command.getInputApp(), command.getInternalOptions(), executor).androidApp);
+    CompilationResult result = runForTesting(
+        command.getInputApp(), command.getInternalOptions(), executor);
+    assert result != null;
+    D8Output output = new D8Output(result.androidApp, command.getOutputMode());
     if (command.getOutputPath() != null) {
       output.write(command.getOutputPath());
     }
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index b4a83a4..a395530 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.OutputMode;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -30,7 +31,6 @@
    * Builder for constructing a D8Command.
    */
   public static class Builder extends BaseCommand.Builder<D8Command, Builder> {
-
     private Builder() {
       super(CompilationMode.DEBUG);
     }
@@ -52,7 +52,8 @@
       if (isPrintHelp() || isPrintVersion()) {
         return new D8Command(isPrintHelp(), isPrintVersion());
       }
-      return new D8Command(getAppBuilder().build(), getOutputPath(), getMode(), getMinApiLevel());
+      return new D8Command(getAppBuilder().build(),
+          getOutputPath(), getOutputMode(), getMode(), getMinApiLevel());
     }
   }
 
@@ -67,6 +68,7 @@
       "  --lib <file>        # Add <file> as a library resource.",
       "  --classpath <file>  # Add <file> as a classpath resource.",
       "  --min-sdk-version   # minimum Android API level compatibility",
+      "  --file-per-class    # produce a separate dex file per class",
       "  --version           # print the version of d8.",
       "  --help              # print this message."));
 
@@ -103,6 +105,8 @@
         }
         builder.setMode(CompilationMode.RELEASE);
         modeSet = CompilationMode.RELEASE;
+      } else if (arg.equals("--file-per-class")) {
+        builder.setOutputMode(OutputMode.FilePerClass);
       } else if (arg.equals("--output")) {
         String output = args[++i];
         if (outputPath != null) {
@@ -126,8 +130,9 @@
     return builder.setOutputPath(outputPath);
   }
 
-  private D8Command(AndroidApp inputApp, Path outputPath, CompilationMode mode, int minApiLevel) {
-    super(inputApp, outputPath, mode, minApiLevel);
+  private D8Command(AndroidApp inputApp, Path outputPath,
+      OutputMode outputMode, CompilationMode mode, int minApiLevel) {
+    super(inputApp, outputPath, outputMode, mode, minApiLevel);
   }
 
   private D8Command(boolean printHelp, boolean printVersion) {
@@ -158,6 +163,7 @@
     internal.outline.enabled = false;
     internal.lazyClasspathLoading = true;
     internal.lazyLibraryLoading = true;
+    internal.outputMode = getOutputMode();
     return internal;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/D8Output.java b/src/main/java/com/android/tools/r8/D8Output.java
index 84f8803..1910e9b 100644
--- a/src/main/java/com/android/tools/r8/D8Output.java
+++ b/src/main/java/com/android/tools/r8/D8Output.java
@@ -4,18 +4,19 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.OutputMode;
 import java.io.IOException;
 import java.nio.file.Path;
 
 /** Represents the output of a D8 compilation. */
 public class D8Output extends BaseOutput {
 
-  D8Output(AndroidApp app) {
-    super(app);
+  D8Output(AndroidApp app, OutputMode outputMode) {
+    super(app, outputMode);
   }
 
   @Override
   public void write(Path output) throws IOException {
-    getAndroidApp().write(output);
+    getAndroidApp().write(output, getOutputMode());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 3b89567..306f37b 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -26,7 +26,6 @@
 import com.android.tools.r8.shaking.AnnotationRemover;
 import com.android.tools.r8.shaking.DiscardedChecker;
 import com.android.tools.r8.shaking.Enqueuer;
-import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.MainDexListBuilder;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.shaking.ProguardTypeMatcher;
@@ -40,6 +39,7 @@
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.AttributeRemovalOptions;
+import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.PackageDistribution;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -384,7 +384,7 @@
     AndroidApp outputApp =
         runForTesting(command.getInputApp(), command.getInternalOptions()).androidApp;
     if (command.getOutputPath() != null) {
-      outputApp.write(command.getOutputPath());
+      outputApp.write(command.getOutputPath(), OutputMode.Indexed);
     }
     return outputApp;
   }
@@ -404,7 +404,7 @@
     AndroidApp outputApp =
         runForTesting(command.getInputApp(), command.getInternalOptions(), executor).androidApp;
     if (command.getOutputPath() != null) {
-      outputApp.write(command.getOutputPath());
+      outputApp.write(command.getOutputPath(), OutputMode.Indexed);
     }
     return outputApp;
   }
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index f70a295..9b44546 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OutputMode;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -158,6 +159,7 @@
       return new R8Command(
           getAppBuilder().build(),
           getOutputPath(),
+          getOutputMode(),
           mainDexKeepRules,
           configuration,
           getMode(),
@@ -296,6 +298,7 @@
   private R8Command(
       AndroidApp inputApp,
       Path outputPath,
+      OutputMode outputMode,
       ImmutableList<ProguardConfigurationRule> mainDexKeepRules,
       ProguardConfiguration proguardConfiguration,
       CompilationMode mode,
@@ -303,9 +306,10 @@
       boolean useTreeShaking,
       boolean useMinification,
       boolean ignoreMissingClasses) {
-    super(inputApp, outputPath, mode, minApiLevel);
+    super(inputApp, outputPath, outputMode, mode, minApiLevel);
     assert proguardConfiguration != null;
     assert mainDexKeepRules != null;
+    assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
     this.mainDexKeepRules = mainDexKeepRules;
     this.proguardConfiguration = proguardConfiguration;
     this.useTreeShaking = useTreeShaking;
@@ -373,6 +377,7 @@
     internal.mainDexKeepRules = mainDexKeepRules;
     internal.keepRules = proguardConfiguration.getRules();
     internal.dontWarnPatterns = proguardConfiguration.getDontWarnPatterns();
+    internal.outputMode = getOutputMode();
     return internal;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/Resource.java b/src/main/java/com/android/tools/r8/Resource.java
index 74236a0..c7760bb 100644
--- a/src/main/java/com/android/tools/r8/Resource.java
+++ b/src/main/java/com/android/tools/r8/Resource.java
@@ -6,6 +6,7 @@
 import com.google.common.io.Closer;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Set;
 
 /** Represents application resources. */
 public interface Resource {
@@ -18,6 +19,12 @@
   /** Get the kind of the resource. */
   Kind getKind();
 
+  /**
+   * Returns the set of class descriptors for classes represented
+   * by the resource if known, or `null' otherwise.
+   */
+  Set<String> getClassDescriptors();
+
   /** Get the resource as a stream. */
   InputStream getStream(Closer closer) throws IOException;
 }
diff --git a/src/main/java/com/android/tools/r8/bisect/Bisect.java b/src/main/java/com/android/tools/r8/bisect/Bisect.java
index b48f268..1c76f46 100644
--- a/src/main/java/com/android/tools/r8/bisect/Bisect.java
+++ b/src/main/java/com/android/tools/r8/bisect/Bisect.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.io.CharStreams;
 import java.io.File;
@@ -180,7 +181,7 @@
     AppInfo appInfo = new AppInfo(app);
     ApplicationWriter writer = new ApplicationWriter(app, appInfo, options, null, null);
     AndroidApp outApp = writer.write(null, executor);
-    outApp.writeToDirectory(output);
+    outApp.writeToDirectory(output, OutputMode.Indexed);
   }
 
   public static void main(String[] args) throws Exception {
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 8ab8921..4bf5cd9 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -91,14 +91,14 @@
       reader.read(DEFAULT_DEX_FILENAME, Resource.Kind.PROGRAM, input.getStream(closer));
     }
     for (InternalResource input : inputApp.getClassClasspathResources()) {
-      if (options.lazyClasspathLoading && input.getClassDescriptor() != null) {
+      if (options.lazyClasspathLoading && input.getSingleClassDescriptorOrNull() != null) {
         addLazyLoader(application, builder, input);
       } else {
         reader.read(DEFAULT_DEX_FILENAME, Resource.Kind.CLASSPATH, input.getStream(closer));
       }
     }
     for (InternalResource input : inputApp.getClassLibraryResources()) {
-      if (options.lazyLibraryLoading && input.getClassDescriptor() != null) {
+      if (options.lazyLibraryLoading && input.getSingleClassDescriptorOrNull() != null) {
         addLazyLoader(application, builder, input);
       } else {
         reader.read(DEFAULT_DEX_FILENAME, Resource.Kind.LIBRARY, input.getStream(closer));
@@ -109,7 +109,7 @@
   private void addLazyLoader(JarApplicationReader application,
       DexApplication.Builder builder, InternalResource resource) {
     // Generate expected DEX type.
-    String classDescriptor = resource.getClassDescriptor();
+    String classDescriptor = resource.getSingleClassDescriptorOrNull();
     assert classDescriptor != null;
     DexType type = options.itemFactory.createType(classDescriptor);
     LazyClassFileLoader newLoader = new LazyClassFileLoader(type, resource, application);
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 d86f929..db264ca 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -19,14 +19,12 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.PackageDistribution;
-import com.android.tools.r8.utils.ThreadUtils;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.io.Writer;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -117,18 +115,23 @@
           VirtualFile.fileSetFrom(this, packageDistribution, executorService);
 
       // Write the dex files and the Proguard mapping file in parallel.
-      List<Future<byte[]>> dexDataFutures = new ArrayList<>();
+      LinkedHashMap<VirtualFile, Future<byte[]>> dexDataFutures = new LinkedHashMap<>();
       for (Integer index : newFiles.keySet()) {
         VirtualFile newFile = newFiles.get(index);
         if (!newFile.isEmpty()) {
-          dexDataFutures.add(executorService.submit(() -> writeDexFile(newFile)));
+          dexDataFutures.put(newFile, executorService.submit(() -> writeDexFile(newFile)));
         }
       }
 
       // Wait for all the spawned futures to terminate.
-      List<byte[]> dexData = ThreadUtils.awaitFutures(dexDataFutures);
       AndroidApp.Builder builder = AndroidApp.builder();
-      dexData.forEach(builder::addDexProgramData);
+      try {
+        for (Map.Entry<VirtualFile, Future<byte[]>> entry : dexDataFutures.entrySet()) {
+          builder.addDexProgramData(entry.getValue().get(), entry.getKey().getClassDescriptors());
+        }
+      } catch (InterruptedException e) {
+        throw new RuntimeException("Interrupted while waiting for future.", e);
+      }
       // Write the proguard map file after writing the dex files, as the map writer traverses
       // the DexProgramClass structures, which are destructively updated during dex file writing.
       byte[] proguardMapResult = writeProguardMapFile();
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 e575cee..ad5420d 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.PackageDistribution;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.google.common.collect.Iterators;
@@ -37,6 +38,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
@@ -61,6 +63,7 @@
   private static final int MAX_PREFILL_ENTRIES = MAX_ENTRIES - 5000;
 
   private final int id;
+  private final Set<String> classDescriptors = new HashSet<>();
   private final VirtualFileIndexedItemCollection indexedItems;
   private final IndexedItemTransaction transaction;
 
@@ -70,6 +73,10 @@
     this.transaction = new IndexedItemTransaction(indexedItems, namingLens);
   }
 
+  public Set<String> getClassDescriptors() {
+    return classDescriptors;
+  }
+
   public static Map<Integer, VirtualFile> fileSetFrom(
       ApplicationWriter writer,
       PackageDistribution packageDistribution,
@@ -81,6 +88,20 @@
     DexApplication application = writer.application;
     InternalOptions options = writer.options;
     Map<Integer, VirtualFile> nameToFileMap = new LinkedHashMap<>();
+
+    if (options.outputMode == OutputMode.FilePerClass) {
+      assert packageDistribution == null :
+          "Cannot combine package distribution definition with file-per-class option.";
+      // Assign dedicated virtual files for all program classes.
+      for (DexProgramClass clazz : application.classes()) {
+        VirtualFile file = new VirtualFile(nameToFileMap.size(), writer.namingLens);
+        nameToFileMap.put(nameToFileMap.size(), file);
+        file.addClass(clazz);
+        file.commitTransaction();
+      }
+      return nameToFileMap;
+    }
+
     if (packageDistribution != null) {
       int maxReferencedIndex = packageDistribution.maxReferencedIndex();
       for (int index = 0; index <= maxReferencedIndex; index++) {
@@ -268,6 +289,7 @@
 
   private void addClass(DexProgramClass clazz) {
     transaction.addClassAndDependencies(clazz);
+    classDescriptors.add(clazz.type.descriptor.toString());
   }
 
   private static boolean isFull(int numberOfMethods, int numberOfFields, int maximum) {
diff --git a/src/main/java/com/android/tools/r8/graph/LazyClassFileLoader.java b/src/main/java/com/android/tools/r8/graph/LazyClassFileLoader.java
index d5fa09a..ce40d19 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyClassFileLoader.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyClassFileLoader.java
@@ -83,11 +83,11 @@
       JarClassFileReader reader = new JarClassFileReader(this.reader, this::addClass);
       reader.read(DEFAULT_DEX_FILENAME, resource.getKind(), resource.getStream(closer));
     } catch (IOException e) {
-      throw new CompilationError("Failed to load class: " + resource.getClassDescriptor(), e);
+      throw new CompilationError("Failed to load class: " + type.toSourceString(), e);
     }
 
     if (loadedClass == null) {
-      throw new Unreachable("Class is supposed to be loaded: " + resource.getClassDescriptor());
+      throw new Unreachable("Class is supposed to be loaded: " + type.toSourceString());
     }
 
     if (loadedClass.type != type) {
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 85d5c57..5c15109 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -31,7 +31,9 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipException;
 import java.util.zip.ZipInputStream;
@@ -234,51 +236,45 @@
   /**
    * Write the dex program resources and proguard resource to @code{output}.
    */
-  public void write(Path output) throws IOException {
-    write(output, false);
+  public void write(Path output, OutputMode outputMode) throws IOException {
+    write(output, outputMode, false);
   }
 
   /**
    * Write the dex program resources and proguard resource to @code{output}.
    */
-  public void write(Path output, boolean overwrite) throws IOException {
+  public void write(Path output, OutputMode outputMode, boolean overwrite) throws IOException {
     if (isArchive(output)) {
-      writeToZip(output);
+      writeToZip(output, outputMode, overwrite);
     } else {
-      writeToDirectory(output);
+      writeToDirectory(output, outputMode, overwrite);
     }
   }
 
   /**
    * Write the dex program resources and proguard resource to @code{directory}.
    */
-  public void writeToDirectory(Path directory) throws IOException {
-    writeToDirectory(directory, false);
+  public void writeToDirectory(Path directory, OutputMode outputMode) throws IOException {
+    writeToDirectory(directory, outputMode, false);
   }
 
   /**
    * Write the dex program resources and proguard resource to @code{directory}.
    */
-  public void writeToDirectory(Path directory, boolean overwrite) throws IOException {
+  public void writeToDirectory(
+      Path directory, OutputMode outputMode, boolean overwrite) throws IOException {
     CopyOption[] options = copyOptions(overwrite);
     try (Closer closer = Closer.create()) {
       List<InternalResource> dexProgramSources = getDexProgramResources();
       for (int i = 0; i < dexProgramSources.size(); i++) {
-        Files.copy(dexProgramSources.get(i).getStream(closer),
-            directory.resolve(getDexFileName(i)), options);
+        Path fileName = directory.resolve(outputMode.getFileName(dexProgramSources.get(i), i));
+        Files.copy(dexProgramSources.get(i).getStream(closer), fileName, options);
       }
       writeProguardMap(closer, directory, overwrite);
       writeProguardSeeds(closer, directory, overwrite);
     }
   }
 
-  /**
-   * Write the dex program resources to @code{archive} and the proguard resource as its sibling.
-   */
-  public void writeToZip(Path archive) throws IOException {
-    writeToZip(archive, false);
-  }
-
   public List<byte[]> writeToMemory() throws IOException {
     List<byte[]> dex = new ArrayList<>();
     try (Closer closer = Closer.create()) {
@@ -296,13 +292,21 @@
   /**
    * Write the dex program resources to @code{archive} and the proguard resource as its sibling.
    */
-  public void writeToZip(Path archive, boolean overwrite) throws IOException {
+  public void writeToZip(Path archive, OutputMode outputMode) throws IOException {
+    writeToZip(archive, outputMode, false);
+  }
+
+  /**
+   * Write the dex program resources to @code{archive} and the proguard resource as its sibling.
+   */
+  public void writeToZip(
+      Path archive, OutputMode outputMode, boolean overwrite) throws IOException {
     OpenOption[] options = openOptions(overwrite);
     try (Closer closer = Closer.create()) {
       try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(archive, options))) {
         List<InternalResource> dexProgramSources = getDexProgramResources();
         for (int i = 0; i < dexProgramSources.size(); i++) {
-          ZipEntry zipEntry = new ZipEntry(getDexFileName(i));
+          ZipEntry zipEntry = new ZipEntry(outputMode.getFileName(dexProgramSources.get(i), i));
           byte[] bytes = ByteStreams.toByteArray(dexProgramSources.get(i).getStream(closer));
           zipEntry.setSize(bytes.length);
           out.putNextEntry(zipEntry);
@@ -338,10 +342,6 @@
     }
   }
 
-  private String getDexFileName(int index) {
-    return index == 0 ? "classes.dex" : ("classes" + (index + 1) + ".dex");
-  }
-
   private OpenOption[] openOptions(boolean overwrite) {
     return new OpenOption[]{
         overwrite ? StandardOpenOption.CREATE : StandardOpenOption.CREATE_NEW,
@@ -456,6 +456,14 @@
     }
 
     /**
+     * Add dex program-data with class descriptor.
+     */
+    public Builder addDexProgramData(byte[] data, Set<String> classDescriptors) {
+      dexSources.add(InternalResource.fromBytes(Resource.Kind.PROGRAM, data, classDescriptors));
+      return this;
+    }
+
+    /**
      * Add dex program-data.
      */
     public Builder addDexProgramData(byte[]... data) {
@@ -577,8 +585,9 @@
             dexSources.add(InternalResource.fromBytes(kind, ByteStreams.toByteArray(stream)));
           } else if (isClassFile(name)) {
             containsClassData = true;
+            String descriptor = guessTypeDescriptor(name);
             classSources.add(InternalResource.fromBytes(
-                kind, ByteStreams.toByteArray(stream), guessTypeDescriptor(name)));
+                kind, ByteStreams.toByteArray(stream), Collections.singleton(descriptor)));
           }
         }
       } catch (ZipException e) {
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 7f13315..3f8d992 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -57,6 +57,9 @@
   // Defines try-with-resources rewriter behavior.
   public OffOrAuto tryWithResourcesDesugaring = OffOrAuto.Off;
 
+  // Application writing mode.
+  public OutputMode outputMode = OutputMode.Indexed;
+
   public boolean useTreeShaking = true;
 
   public boolean printCfg = false;
diff --git a/src/main/java/com/android/tools/r8/utils/InternalResource.java b/src/main/java/com/android/tools/r8/utils/InternalResource.java
index 2d9bdb9..9181092 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalResource.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalResource.java
@@ -11,8 +11,10 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Path;
+import java.util.Set;
 
-/** Internal resource class that is not intended for use from the outside.
+/**
+ * Internal resource class that is not intended for use from the outside.
  *
  * <p>This is only here to hide the creation and class descriptor methods
  * from the javadoc for the D8 API. If we decide to expose those methods
@@ -26,9 +28,6 @@
     this.kind = kind;
   }
 
-  /** Returns class descriptor if known, otherwise returns null. */
-  public abstract String getClassDescriptor();
-
   /** Get the kind of the resource. */
   public Kind getKind() {
     return kind;
@@ -45,8 +44,18 @@
   }
 
   /** Create an application resource for a given content, kind and type descriptor. */
-  public static InternalResource fromBytes(Kind kind, byte[] bytes, String typeDescriptor) {
-    return new ByteResource(kind, bytes, typeDescriptor);
+  public static InternalResource fromBytes(Kind kind, byte[] bytes, Set<String> typeDescriptors) {
+    return new ByteResource(kind, bytes, typeDescriptors);
+  }
+
+  /**
+   * If the resource represents a single class returns
+   * its descriptor, returns `null` otherwise.
+   */
+  public String getSingleClassDescriptorOrNull() {
+    Set<String> descriptors = getClassDescriptors();
+    return (descriptors == null) || (descriptors.size() != 1)
+        ? null : descriptors.iterator().next();
   }
 
   /** File based application resource. */
@@ -60,7 +69,7 @@
     }
 
     @Override
-    public String getClassDescriptor() {
+    public Set<String> getClassDescriptors() {
       return null;
     }
 
@@ -72,19 +81,19 @@
 
   /** Byte content based application resource. */
   private static class ByteResource extends InternalResource {
-    final String classDescriptor;
+    final Set<String> classDescriptors;
     final byte[] bytes;
 
-    ByteResource(Kind kind, byte[] bytes, String classDescriptor) {
+    ByteResource(Kind kind, byte[] bytes, Set<String> classDescriptors) {
       super(kind);
       assert bytes != null;
-      this.classDescriptor = classDescriptor;
+      this.classDescriptors = classDescriptors;
       this.bytes = bytes;
     }
 
     @Override
-    public String getClassDescriptor() {
-      return classDescriptor;
+    public Set<String> getClassDescriptors() {
+      return classDescriptors;
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/utils/OutputMode.java b/src/main/java/com/android/tools/r8/utils/OutputMode.java
new file mode 100644
index 0000000..c27429a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/OutputMode.java
@@ -0,0 +1,28 @@
+// 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.utils;
+
+import com.android.tools.r8.Resource;
+
+/** Defines way the output is formed. */
+public enum OutputMode {
+  Indexed {
+    @Override
+    String getFileName(Resource resource, int index) {
+      return index == 0 ? "classes.dex" : ("classes" + (index + 1) + ".dex");
+    }
+  },
+  FilePerClass {
+    @Override
+    String getFileName(Resource resource, int index) {
+      assert resource instanceof InternalResource;
+      String classDescriptor = ((InternalResource) resource).getSingleClassDescriptorOrNull();
+      assert classDescriptor != null;
+      assert !classDescriptor.contains(".");
+      return DescriptorUtils.descriptorToJavaType(classDescriptor) + ".dex";
+    }
+  };
+
+  abstract String getFileName(Resource resource, int index);
+}
diff --git a/src/test/examplesAndroidO/incremental/IncrementallyCompiled.java b/src/test/examplesAndroidO/incremental/IncrementallyCompiled.java
new file mode 100644
index 0000000..848528e
--- /dev/null
+++ b/src/test/examplesAndroidO/incremental/IncrementallyCompiled.java
@@ -0,0 +1,20 @@
+// 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 incremental;
+
+public class IncrementallyCompiled {
+  class A implements B {
+    class AB implements B.BA {
+    }
+  }
+
+  interface B {
+    interface BA {
+    }
+  }
+
+  static class C {
+  }
+}
+
diff --git a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
index a5e944a..4fd3276 100644
--- a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
@@ -4,16 +4,33 @@
 
 package com.android.tools.r8;
 
+import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
+
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalResource;
+import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.OutputMode;
+import com.beust.jcommander.internal.Lists;
+import com.google.common.io.Closer;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
 import java.util.function.UnaryOperator;
+import org.junit.Assert;
+import org.junit.Test;
 
 public class D8IncrementalRunExamplesAndroidOTest
     extends RunExamplesAndroidOTest<D8Command.Builder> {
@@ -31,18 +48,53 @@
 
     @Override
     void build(Path testJarFile, Path out) throws Throwable {
-      // Collect classes and compile separately.
-      List<String> classFiles = collectClassFiles(testJarFile);
-      List<String> dexFiles = new ArrayList<>();
-      for (int i = 0; i < classFiles.size(); i++) {
-        Path indexedOut = Paths.get(
-            out.getParent().toString(), out.getFileName() + "." + i + ".zip");
-        compile(testJarFile.toString(), Collections.singletonList(classFiles.get(i)), indexedOut);
-        dexFiles.add(indexedOut.toString());
-      }
+      Map<String, Resource> files = compileClassesTogether(testJarFile, null);
+      mergeClassFiles(Lists.newArrayList(files.values()), out);
+    }
 
-      // When compiled add files separately, merge them.
-      compile(null, dexFiles, out);
+    // Dex classes separately.
+    SortedMap<String, Resource> compileClassesSeparately(Path testJarFile) throws Throwable {
+      TreeMap<String, Resource> fileToResource = new TreeMap<>();
+      List<String> classFiles = collectClassFiles(testJarFile);
+      for (String classFile : classFiles) {
+        AndroidApp app = compileClassFiles(
+            testJarFile.toString(), Collections.singletonList(classFile), null, OutputMode.Indexed);
+        assert app.getDexProgramResources().size() == 1;
+        fileToResource.put(
+            makeRelative(testJarFile, Paths.get(classFile)).toString(),
+            app.getDexProgramResources().get(0));
+      }
+      return fileToResource;
+    }
+
+    // Dex classes in one D8 invocation.
+    SortedMap<String, Resource> compileClassesTogether(
+        Path testJarFile, Path output) throws Throwable {
+      TreeMap<String, Resource> fileToResource = new TreeMap<>();
+      List<String> classFiles = collectClassFiles(testJarFile);
+      AndroidApp app = compileClassFiles(
+          testJarFile.toString(), classFiles, output, OutputMode.FilePerClass);
+      for (InternalResource resource : app.getDexProgramResources()) {
+        String classDescriptor = resource.getSingleClassDescriptorOrNull();
+        Assert.assertNotNull("Add resources are expected to have a descriptor", classDescriptor);
+        classDescriptor = classDescriptor.substring(1, classDescriptor.length() - 1);
+        fileToResource.put(classDescriptor + ".class", resource);
+      }
+      return fileToResource;
+    }
+
+    private Path makeRelative(Path testJarFile, Path classFile) {
+      classFile = classFile.toAbsolutePath();
+      Path regularParent =
+          testJarFile.getParent().resolve(Paths.get("classes")).toAbsolutePath();
+      Path legacyParent = regularParent.resolve(Paths.get("..",
+          regularParent.getFileName().toString() + "Legacy", "classes")).toAbsolutePath();
+
+      if (classFile.startsWith(regularParent)) {
+        return regularParent.relativize(classFile);
+      }
+      Assert.assertTrue(classFile.startsWith(legacyParent));
+      return legacyParent.relativize(classFile);
     }
 
     private List<String> collectClassFiles(Path testJarFile) {
@@ -73,21 +125,48 @@
       }
     }
 
-    private void compile(String classpath, List<String> inputFiles, Path out) throws Throwable {
+    AndroidApp compileClassFiles(String classpath,
+        List<String> inputFiles, Path output, OutputMode outputMode) throws Throwable {
       D8Command.Builder builder = D8Command.builder();
-      if (classpath != null) {
-        builder.addClasspathFiles(Paths.get(classpath));
-      }
+      builder = builder.addClasspathFiles(Paths.get(classpath));
       for (String inputFile : inputFiles) {
-        builder.addProgramFiles(Paths.get(inputFile));
+        builder = builder.addProgramFiles(Paths.get(inputFile));
       }
       for (UnaryOperator<D8Command.Builder> transformation : builderTransformations) {
         builder = transformation.apply(builder);
       }
-      builder.addLibraryFiles(Paths.get(ToolHelper.getAndroidJar(builder.getMinApiLevel())));
-      D8Command command = builder.setOutputPath(out).build();
+      builder = builder.setOutputMode(outputMode);
+      builder = builder.addLibraryFiles(
+          Paths.get(ToolHelper.getAndroidJar(builder.getMinApiLevel())));
+      if (output != null) {
+        builder = builder.setOutputPath(output);
+      }
+      D8Command command = builder.build();
       try {
-        ToolHelper.runD8(command, this::combinedOptionConsumer);
+        return ToolHelper.runD8(command, this::combinedOptionConsumer);
+      } catch (Unimplemented | CompilationError | InternalCompilerError re) {
+        throw re;
+      } catch (RuntimeException re) {
+        throw re.getCause() == null ? re : re.getCause();
+      }
+    }
+
+    Resource mergeClassFiles(List<Resource> dexFiles, Path out) throws Throwable {
+      D8Command.Builder builder = D8Command.builder();
+      for (Resource dexFile : dexFiles) {
+        builder.addDexProgramData(readFromResource(dexFile));
+      }
+      for (UnaryOperator<D8Command.Builder> transformation : builderTransformations) {
+        builder = transformation.apply(builder);
+      }
+      if (out != null) {
+        builder = builder.setOutputPath(out);
+      }
+      D8Command command = builder.build();
+      try {
+        AndroidApp app = ToolHelper.runD8(command, this::combinedOptionConsumer);
+        assert app.getDexProgramResources().size() == 1;
+        return app.getDexProgramResources().get(0);
       } catch (Unimplemented | CompilationError | InternalCompilerError re) {
         throw re;
       } catch (RuntimeException re) {
@@ -96,8 +175,102 @@
     }
   }
 
+  @Test
+  public void dexPerClassFileNoDesugaring() throws Throwable {
+    String testName = "dexPerClassFileNoDesugaring";
+    String testPackage = "incremental";
+    String mainClass = "IncrementallyCompiled";
+
+    Path inputJarFile = Paths.get(EXAMPLE_DIR, testPackage + JAR_EXTENSION);
+
+    D8IncrementalTestRunner test = test(testName, testPackage, mainClass);
+
+    Map<String, Resource> compiledSeparately = test.compileClassesSeparately(inputJarFile);
+    Map<String, Resource> compiledTogether = test.compileClassesTogether(inputJarFile, null);
+    Assert.assertEquals(compiledSeparately.size(), compiledTogether.size());
+
+    for (Map.Entry<String, Resource> entry : compiledSeparately.entrySet()) {
+      Resource otherResource = compiledTogether.get(entry.getKey());
+      Assert.assertNotNull(otherResource);
+      Assert.assertArrayEquals(readFromResource(entry.getValue()), readFromResource(otherResource));
+    }
+
+    Resource mergedFromCompiledSeparately =
+        test.mergeClassFiles(Lists.newArrayList(compiledSeparately.values()), null);
+    Resource mergedFromCompiledTogether =
+        test.mergeClassFiles(Lists.newArrayList(compiledTogether.values()), null);
+    Assert.assertArrayEquals(
+        readFromResource(mergedFromCompiledSeparately),
+        readFromResource(mergedFromCompiledTogether));
+  }
+
+  @Test
+  public void dexPerClassFileWithDesugaring() throws Throwable {
+    String testName = "dexPerClassFileWithDesugaring";
+    String testPackage = "lambdadesugaringnplus";
+    String mainClass = "LambdasWithStaticAndDefaultMethods";
+
+    Path inputJarFile = Paths.get(EXAMPLE_DIR, testPackage + JAR_EXTENSION);
+
+    D8IncrementalTestRunner test = test(testName, testPackage, mainClass);
+    test.withInterfaceMethodDesugaring(OffOrAuto.Auto);
+
+    Resource mergedFromCompiledSeparately =
+        test.mergeClassFiles(Lists.newArrayList(
+            test.compileClassesSeparately(inputJarFile).values()), null);
+    Resource mergedFromCompiledTogether =
+        test.mergeClassFiles(Lists.newArrayList(
+            test.compileClassesTogether(inputJarFile, null).values()), null);
+
+    Assert.assertArrayEquals(
+        readFromResource(mergedFromCompiledSeparately),
+        readFromResource(mergedFromCompiledTogether));
+  }
+
+  @Test
+  public void dexPerClassFileOutputFiles() throws Throwable {
+    String testName = "dexPerClassFileNoDesugaring";
+    String testPackage = "incremental";
+    String mainClass = "IncrementallyCompiled";
+
+    Path out = temp.getRoot().toPath();
+
+    Path inputJarFile = Paths.get(EXAMPLE_DIR, testPackage + JAR_EXTENSION);
+
+    D8IncrementalTestRunner test = test(testName, testPackage, mainClass);
+    test.compileClassesTogether(inputJarFile, out);
+
+    String[] dexFiles = out.toFile().list();
+    assert dexFiles != null;
+    Arrays.sort(dexFiles);
+
+    String[] expectedFileNames = {
+        "incremental.IncrementallyCompiled$A$AB.dex",
+        "incremental.IncrementallyCompiled$A.dex",
+        "incremental.IncrementallyCompiled$B$BA.dex",
+        "incremental.IncrementallyCompiled$B.dex",
+        "incremental.IncrementallyCompiled$C.dex",
+        "incremental.IncrementallyCompiled.dex"
+    };
+
+    Assert.assertArrayEquals(expectedFileNames, dexFiles);
+  }
+
   @Override
-  TestRunner test(String testName, String packageName, String mainClass) {
+  D8IncrementalTestRunner test(String testName, String packageName, String mainClass) {
     return new D8IncrementalTestRunner(testName, packageName, mainClass);
   }
+
+  static byte[] readFromResource(Resource resource) throws IOException {
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    byte[] buffer = new byte[16384];
+    try (Closer closer = Closer.create()) {
+      InputStream stream = resource.getStream(closer);
+      int read;
+      while ((read = stream.read(buffer, 0, buffer.length)) != -1) {
+        output.write(buffer, 0, read);
+      }
+    }
+    return output.toByteArray();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index bcb9f7d..333e77b 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OutputMode;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.ByteStreams;
 import java.io.File;
@@ -121,7 +122,7 @@
    */
   protected String runOnArt(AndroidApp app, Class mainClass) throws IOException {
     Path out = File.createTempFile("junit", ".zip", temp.getRoot()).toPath();
-    app.writeToZip(out, true);
+    app.writeToZip(out, OutputMode.Indexed, true);
     return ToolHelper.runArtNoVerificationErrors(
         ImmutableList.of(out.toString()), mainClass.getCanonicalName(), null);
   }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 739d90c..425dab7 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -454,7 +455,7 @@
     }
     CompilationResult result = R8.runForTesting(app, options);
     if (command.getOutputPath() != null) {
-      result.androidApp.write(command.getOutputPath());
+      result.androidApp.write(command.getOutputPath(), OutputMode.Indexed);
     }
     return result;
   }
@@ -495,7 +496,7 @@
     }
     AndroidApp result = D8.runForTesting(command.getInputApp(), options).androidApp;
     if (command.getOutputPath() != null) {
-      result.write(command.getOutputPath());
+      result.write(command.getOutputPath(), command.getOutputMode());
     }
     return result;
   }
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
index c16291a..9868437 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.OutputMode;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -49,7 +50,7 @@
 
   protected String runOnArt(AndroidApp app, String main) throws IOException {
     Path out = temp.getRoot().toPath().resolve("out.zip");
-    app.writeToZip(out, true);
+    app.writeToZip(out, OutputMode.Indexed, true);
     return ToolHelper.runArtNoVerificationErrors(ImmutableList.of(out.toString()), main, null);
   }
 
diff --git a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
index 4b2ece0..80a1324 100644
--- a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
+++ b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.utils.ArtErrorParser.ArtErrorParserException;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.OutputMode;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -78,7 +79,7 @@
     }
     Path out = temp.getRoot().toPath().resolve("all.zip");
     Path oatFile = temp.getRoot().toPath().resolve("all.oat");
-    outputApp.writeToZip(out);
+    outputApp.writeToZip(out, OutputMode.Indexed);
     try {
       ToolHelper.runDex2Oat(out, oatFile);
       return outputApp;
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
index 1481bc8..d9314d8 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalResource;
+import com.android.tools.r8.utils.OutputMode;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
 import java.io.IOException;
@@ -57,7 +58,7 @@
     // Check that the generated bytecode runs through the dex2oat verifier with no errors.
     Path combinedInput = temp.getRoot().toPath().resolve("all.jar");
     Path oatFile = temp.getRoot().toPath().resolve("all.oat");
-    app1.writeToZip(combinedInput);
+    app1.writeToZip(combinedInput, OutputMode.Indexed);
     ToolHelper.runDex2Oat(combinedInput, oatFile);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
index 7db5451..615e289 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 import jasmin.ClassFile;
@@ -121,7 +122,7 @@
 
   protected String runOnArt(AndroidApp app, String main) throws IOException {
     Path out = temp.getRoot().toPath().resolve("out.zip");
-    app.writeToZip(out);
+    app.writeToZip(out, OutputMode.Indexed);
     return ToolHelper.runArtNoVerificationErrors(ImmutableList.of(out.toString()), main, null);
   }
 
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 115aabd..29cef19 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -49,6 +49,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.MainDexList;
+import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
@@ -108,7 +109,7 @@
     }
     AndroidApp generated = generateApplication(TWO_LARGE_CLASSES, MAX_METHOD_COUNT);
     if (regenerateApplications) {
-      generated.write(getTwoLargeClassesAppPath(), true);
+      generated.write(getTwoLargeClassesAppPath(), OutputMode.Indexed, true);
     } else {
       AndroidApp cached = AndroidApp.fromProgramFiles(getTwoLargeClassesAppPath());
       compareToCachedVersion(cached, generated, TWO_LARGE_CLASSES_APP);
@@ -123,7 +124,7 @@
     }
     AndroidApp generated = generateApplication(MANY_CLASSES, 1);
     if (regenerateApplications) {
-      generated.write(getManyClassesAppPath(), true);
+      generated.write(getManyClassesAppPath(), OutputMode.Indexed, true);
     } else {
       AndroidApp cached = AndroidApp.fromProgramFiles(getManyClassesAppPath());
       compareToCachedVersion(cached, generated, MANY_CLASSES_APP);
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
index 314adb1..3665a86 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
 import com.android.tools.r8.utils.DexInspector.MethodSubject;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.Smali;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Iterables;
@@ -452,7 +453,7 @@
       AndroidApp app = writeDex(application, options);
       Path out = temp.getRoot().toPath().resolve("run-art-input.zip");
       // TODO(sgjesse): Pass in a unique temp directory for each run.
-      app.writeToZip(out, true);
+      app.writeToZip(out, OutputMode.Indexed, true);
       return ToolHelper.runArtNoVerificationErrors(out.toString(), DEFAULT_MAIN_CLASS_NAME);
     } catch (IOException e) {
       throw new RuntimeException(e);
@@ -464,7 +465,7 @@
       AndroidApp app = writeDex(application, options);
       Path dexOut = temp.getRoot().toPath().resolve("run-dex2oat-input.zip");
       Path oatFile = temp.getRoot().toPath().resolve("oat-file");
-      app.writeToZip(dexOut);
+      app.writeToZip(dexOut, OutputMode.Indexed);
       ToolHelper.runDex2Oat(dexOut, oatFile);
     } catch (IOException e) {
       throw new RuntimeException(e);