Introduce dedicated dex-file consumers.

The consumers for DEX files are split into two. One for indexed mode and one for
file-per-class-file mode. The type of consumer defines what output-mode
compilation is in. The explicit output-mode and output-path getters and setters
are deprecated and will be removed once the API supports setting up the
consumers directly.

Change-Id: I58ade03d221f1571a8db79f7f7bc573f8a479c8b
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
index 33f9aeb..4a2f7b0 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -3,17 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DefaultDiagnosticsHandler;
-import com.android.tools.r8.utils.FileSystemOutputSink;
 import com.android.tools.r8.utils.FileUtils;
-import com.android.tools.r8.utils.IOExceptionDiagnostic;
-import com.android.tools.r8.utils.IgnoreContentsOutputSink;
 import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.Reporter;
-import java.io.IOException;
 import java.nio.file.Path;
 
 /**
@@ -23,18 +18,15 @@
  */
 abstract class BaseCompilerCommand extends BaseCommand {
 
-  private final Path outputPath;
-  private final OutputMode outputMode;
+  private final ProgramConsumer programConsumer;
   private final CompilationMode mode;
   private final int minApiLevel;
   private final Reporter reporter;
   private final boolean enableDesugaring;
-  private OutputSink outputSink;
 
   BaseCompilerCommand(boolean printHelp, boolean printVersion) {
     super(printHelp, printVersion);
-    outputPath = null;
-    outputMode = OutputMode.Indexed;
+    programConsumer = null;
     mode = null;
     minApiLevel = 0;
     reporter = new Reporter(new DefaultDiagnosticsHandler());
@@ -43,25 +35,24 @@
 
   BaseCompilerCommand(
       AndroidApp app,
-      Path outputPath,
-      OutputMode outputMode,
       CompilationMode mode,
+      ProgramConsumer programConsumer,
       int minApiLevel,
       Reporter reporter,
       boolean enableDesugaring) {
     super(app);
-    assert mode != null;
     assert minApiLevel > 0;
-    this.outputPath = outputPath;
-    this.outputMode = outputMode;
+    assert mode != null;
     this.mode = mode;
+    this.programConsumer = programConsumer;
     this.minApiLevel = minApiLevel;
     this.reporter = reporter;
     this.enableDesugaring = enableDesugaring;
   }
 
+  @Deprecated
   public Path getOutputPath() {
-    return outputPath;
+    return programConsumer == null ? null : programConsumer.getOutputPath();
   }
 
   public CompilationMode getMode() {
@@ -72,35 +63,21 @@
     return minApiLevel;
   }
 
+  @Deprecated
   public OutputMode getOutputMode() {
-    return outputMode;
+    return programConsumer instanceof DexFilePerClassFileConsumer
+        ? OutputMode.FilePerInputClass
+        : OutputMode.Indexed;
+  }
+
+  public ProgramConsumer getProgramConsumer() {
+    return programConsumer;
   }
 
   public Reporter getReporter() {
     return reporter;
   }
 
-  private OutputSink createOutputSink() {
-    if (outputPath == null) {
-      return new IgnoreContentsOutputSink();
-    } else {
-      try {
-        // TODO(zerny): Calling getInternalOptions here is incorrect since any modifications by an
-        // options consumer will not be visible to the sink.
-        return FileSystemOutputSink.create(outputPath, getInternalOptions());
-      } catch (IOException e) {
-        throw reporter.fatalError(new IOExceptionDiagnostic(e, new PathOrigin(outputPath)));
-      }
-    }
-  }
-
-  public OutputSink getOutputSink() {
-    if (outputSink == null) {
-      outputSink = createOutputSink();
-    }
-    return outputSink;
-  }
-
   public boolean getEnableDesugaring() {
     return enableDesugaring;
   }
@@ -160,22 +137,26 @@
       return outputMode;
     }
 
-    /**
-     * Set an output path. Must be an existing directory or a zip file.
-     */
+    /** Set an output path. Must be an existing directory or a zip file. */
+    @Deprecated
     public B setOutputPath(Path outputPath) {
       this.outputPath = outputPath;
       return self();
     }
 
-    /**
-     * Set an output mode.
-     */
+    /** Set an output mode. */
+    @Deprecated
     public B setOutputMode(OutputMode outputMode) {
       this.outputMode = outputMode;
       return self();
     }
 
+    public ProgramConsumer getProgramConsumer() {
+      return getOutputMode() == OutputMode.Indexed
+          ? createIndexedConsumer()
+          : createPerClassFileConsumer();
+    }
+
     /**
      * Get the minimum API level (aka SDK version).
      */
@@ -210,5 +191,19 @@
       FileUtils.validateOutputFile(outputPath, reporter);
       super.validate();
     }
+
+    protected DexIndexedConsumer createIndexedConsumer() {
+      Path path = getOutputPath();
+      return FileUtils.isArchive(path)
+          ? new DexIndexedConsumer.ArchiveConsumer(path)
+          : new DexIndexedConsumer.DirectoryConsumer(path);
+    }
+
+    protected DexFilePerClassFileConsumer createPerClassFileConsumer() {
+      Path path = getOutputPath();
+      return FileUtils.isArchive(path)
+          ? new DexFilePerClassFileConsumer.ArchiveConsumer(path)
+          : new DexFilePerClassFileConsumer.DirectoryConsumer(path);
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ClassFileConsumer.java b/src/main/java/com/android/tools/r8/ClassFileConsumer.java
index f55e362..eea50af 100644
--- a/src/main/java/com/android/tools/r8/ClassFileConsumer.java
+++ b/src/main/java/com/android/tools/r8/ClassFileConsumer.java
@@ -40,22 +40,15 @@
   void accept(byte[] data, String descriptor, DiagnosticsHandler handler);
 
   /** Empty consumer to request the production of the resource but ignore its value. */
-  class EmptyConsumer implements ClassFileConsumer {
-
-    @Override
-    public void accept(byte[] data, String descriptor, DiagnosticsHandler handler) {
-      // Ignore data.
-    }
-
-    @Override
-    public void finished(DiagnosticsHandler handler) {
-      // Nothing to close.
-    }
+  static ClassFileConsumer emptyConsumer() {
+    return ForwardingConsumer.EMPTY_CONSUMER;
   }
 
   /** Forwarding consumer to delegate to an optional existing consumer. */
   class ForwardingConsumer implements ClassFileConsumer {
 
+    private static final ClassFileConsumer EMPTY_CONSUMER = new ForwardingConsumer(null);
+
     private final ClassFileConsumer consumer;
 
     public ForwardingConsumer(ClassFileConsumer consumer) {
@@ -75,6 +68,11 @@
         consumer.finished(handler);
       }
     }
+
+    @Override
+    public Path getOutputPath() {
+      return consumer == null ? null : consumer.getOutputPath();
+    }
   }
 
   /** Archive consumer to write program resources to a zip archive. */
@@ -116,6 +114,11 @@
       }
     }
 
+    @Override
+    public Path getOutputPath() {
+      return archive;
+    }
+
     private ZipOutputStream getStream(DiagnosticsHandler handler) {
       assert !closed;
       if (stream == null) {
@@ -176,6 +179,11 @@
       super.finished(handler);
     }
 
+    @Override
+    public Path getOutputPath() {
+      return directory;
+    }
+
     private static void writeFileFromDescriptor(byte[] contents, Path target) throws IOException {
       Files.createDirectories(target.getParent());
       FileUtils.writeToFile(target, null, contents);
diff --git a/src/main/java/com/android/tools/r8/CompilationResult.java b/src/main/java/com/android/tools/r8/CompilationResult.java
index 283562b..9a81b52 100644
--- a/src/main/java/com/android/tools/r8/CompilationResult.java
+++ b/src/main/java/com/android/tools/r8/CompilationResult.java
@@ -8,12 +8,10 @@
 
 public class CompilationResult {
 
-  public final OutputSink outputSink;
   public final DexApplication dexApplication;
   public final AppInfo appInfo;
 
-  public CompilationResult(OutputSink outputSink, DexApplication dexApplication, AppInfo appInfo) {
-    this.outputSink = outputSink;
+  public CompilationResult(DexApplication dexApplication, AppInfo appInfo) {
     this.dexApplication = dexApplication;
     this.appInfo = appInfo;
   }
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 0e7f329..f05e4c6 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -16,7 +16,7 @@
 import com.android.tools.r8.origin.CommandLineOrigin;
 import com.android.tools.r8.utils.AbortException;
 import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.AndroidAppOutputSink;
+import com.android.tools.r8.utils.AndroidAppConsumers;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.IOExceptionDiagnostic;
 import com.android.tools.r8.utils.InternalOptions;
@@ -93,10 +93,8 @@
     try {
       try {
         InternalOptions options = command.getInternalOptions();
-        AndroidAppOutputSink compatSink =
-            new AndroidAppOutputSink(command.getOutputSink(), options);
-        CompilationResult result =
-            run(command.getInputApp(), compatSink, options, executor);
+        AndroidAppConsumers compatSink = new AndroidAppConsumers(options);
+        CompilationResult result = run(command.getInputApp(), options, executor);
         assert result != null;
         D8Output d8Output = new D8Output(compatSink.build(), command.getOutputMode());
         command.getReporter().failIfPendingErrors();
@@ -126,7 +124,7 @@
       Version.printToolVersion("D8");
       return;
     }
-    runForTesting(command.getInputApp(), command.getOutputSink(), command.getInternalOptions());
+    runForTesting(command.getInputApp(), command.getInternalOptions());
   }
 
   /** Command-line entry to D8. */
@@ -161,12 +159,11 @@
     }
   }
 
-  static CompilationResult runForTesting(AndroidApp inputApp, OutputSink outputSink,
-      InternalOptions options)
+  static CompilationResult runForTesting(AndroidApp inputApp, InternalOptions options)
       throws IOException, CompilationException {
     ExecutorService executor = ThreadUtils.getExecutorService(ThreadUtils.NOT_SPECIFIED);
     try {
-      return run(inputApp, outputSink, options, executor);
+      return run(inputApp, options, executor);
     } finally {
       executor.shutdown();
     }
@@ -187,8 +184,7 @@
   }
 
   private static CompilationResult run(
-      AndroidApp inputApp, OutputSink outputSink, InternalOptions options,
-      ExecutorService executor)
+      AndroidApp inputApp, InternalOptions options, ExecutorService executor)
       throws IOException, CompilationException {
     try {
       // Disable global optimizations.
@@ -209,16 +205,15 @@
       }
       Marker marker = getMarker(options);
       new ApplicationWriter(app, options, marker, null, NamingLens.getIdentityLens(), null, null)
-          .write(outputSink, executor);
-      CompilationResult output = new CompilationResult(outputSink, app, appInfo);
+          .write(executor);
+      CompilationResult output = new CompilationResult(app, appInfo);
       options.printWarnings();
       return output;
     } catch (ExecutionException e) {
       R8.unwrapExecutionException(e);
       throw new AssertionError(e); // unwrapping method should have thrown
     } finally {
-      options.closeProgramConsumer();
-      outputSink.close();
+      options.signalFinishedToProgramConsumer();
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 35aa36c..235b165 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -109,14 +109,27 @@
 
       return new D8Command(
           getAppBuilder().build(),
-          getOutputPath(),
-          getOutputMode(),
           getMode(),
+          getProgramConsumer(),
           getMinApiLevel(),
           reporter,
           getEnableDesugaring(),
           intermediate);
     }
+
+    @Override
+    protected DexIndexedConsumer createIndexedConsumer() {
+      return getOutputPath() == null
+          ? DexIndexedConsumer.emptyConsumer()
+          : super.createIndexedConsumer();
+    }
+
+    @Override
+    protected DexFilePerClassFileConsumer createPerClassFileConsumer() {
+      return getOutputPath() == null
+          ? DexFilePerClassFileConsumer.emptyConsumer()
+          : super.createPerClassFileConsumer();
+    }
   }
 
   static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of(
@@ -221,15 +234,13 @@
 
   private D8Command(
       AndroidApp inputApp,
-      Path outputPath,
-      OutputMode outputMode,
       CompilationMode mode,
+      ProgramConsumer programConsumer,
       int minApiLevel,
       Reporter diagnosticsHandler,
       boolean enableDesugaring,
       boolean intermediate) {
-    super(inputApp, outputPath, outputMode, mode, minApiLevel, diagnosticsHandler,
-        enableDesugaring);
+    super(inputApp, mode, programConsumer, minApiLevel, diagnosticsHandler, enableDesugaring);
     this.intermediate = intermediate;
   }
 
@@ -242,6 +253,7 @@
     InternalOptions internal = new InternalOptions(new DexItemFactory(), getReporter());
     assert !internal.debug;
     internal.debug = getMode() == CompilationMode.DEBUG;
+    internal.programConsumer = getProgramConsumer();
     internal.minimalMainDex = internal.debug;
     internal.minApiLevel = getMinApiLevel();
     internal.intermediate = intermediate;
@@ -261,7 +273,6 @@
     assert internal.propagateMemberValue;
     internal.propagateMemberValue = false;
 
-    internal.outputMode = getOutputMode();
     internal.enableDesugaring = getEnableDesugaring();
     return internal;
   }
diff --git a/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumer.java b/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumer.java
new file mode 100644
index 0000000..bb3b16b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumer.java
@@ -0,0 +1,252 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.IOExceptionDiagnostic;
+import com.android.tools.r8.utils.ZipUtils;
+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.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Consumer for DEX encoded programs.
+ *
+ * <p>This consumer receives DEX file content for each Java class-file input.
+ */
+public interface DexFilePerClassFileConsumer extends ProgramConsumer {
+
+  /**
+   * Callback to receive DEX data for a single Java class-file input and its companion classes.
+   *
+   * <p>There is no guaranteed order and files might be written concurrently.
+   *
+   * <p>The consumer is expected not to throw, but instead report any errors via the diagnostics
+   * {@param handler}. If an error is reported via {@param handler} and no exceptions are thrown,
+   * then the compiler guaranties to exit with an error.
+   *
+   * @param primaryClassDescriptor Class descriptor of the class from the input class-file.
+   * @param data DEX encoded data.
+   * @param descriptors Class descriptors for all classes defined in the DEX data.
+   * @param handler Diagnostics handler for reporting.
+   */
+  void accept(
+      String primaryClassDescriptor,
+      byte[] data,
+      Set<String> descriptors,
+      DiagnosticsHandler handler);
+
+  /** Empty consumer to request the production of the resource but ignore its value. */
+  static DexFilePerClassFileConsumer emptyConsumer() {
+    return ForwardingConsumer.EMPTY_CONSUMER;
+  }
+
+  /** Forwarding consumer to delegate to an optional existing consumer. */
+  class ForwardingConsumer implements DexFilePerClassFileConsumer {
+
+    private static final DexFilePerClassFileConsumer EMPTY_CONSUMER = new ForwardingConsumer(null);
+
+    private final DexFilePerClassFileConsumer consumer;
+
+    public ForwardingConsumer(DexFilePerClassFileConsumer consumer) {
+      this.consumer = consumer;
+    }
+
+    @Override
+    public void accept(
+        String primaryClassDescriptor,
+        byte[] data,
+        Set<String> descriptors,
+        DiagnosticsHandler handler) {
+      if (consumer != null) {
+        consumer.accept(primaryClassDescriptor, data, descriptors, handler);
+      }
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      if (consumer != null) {
+        consumer.finished(handler);
+      }
+    }
+
+    @Override
+    public Path getOutputPath() {
+      return consumer == null ? null : consumer.getOutputPath();
+    }
+  }
+
+  /** Archive consumer to write program resources to a zip archive. */
+  class ArchiveConsumer extends ForwardingConsumer {
+
+    private static String getDexFileName(String classDescriptor) {
+      assert classDescriptor != null && DescriptorUtils.isClassDescriptor(classDescriptor);
+      return DescriptorUtils.getClassBinaryNameFromDescriptor(classDescriptor) + DEX_EXTENSION;
+    }
+
+    private final Path archive;
+    private final Origin origin;
+    private ZipOutputStream stream = null;
+    private boolean closed = false;
+
+    public ArchiveConsumer(Path archive) {
+      this(archive, null);
+    }
+
+    public ArchiveConsumer(Path archive, DexFilePerClassFileConsumer consumer) {
+      super(consumer);
+      this.archive = archive;
+      origin = new PathOrigin(archive);
+    }
+
+    @Override
+    public void accept(
+        String primaryClassDescriptor,
+        byte[] data,
+        Set<String> descriptors,
+        DiagnosticsHandler handler) {
+      super.accept(primaryClassDescriptor, data, descriptors, handler);
+      synchronizedWrite(getDexFileName(primaryClassDescriptor), data, handler);
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      super.finished(handler);
+      assert !closed;
+      closed = true;
+      try {
+        if (stream != null) {
+          stream.close();
+          stream = null;
+        }
+      } catch (IOException e) {
+        handler.error(new IOExceptionDiagnostic(e, origin));
+      }
+    }
+
+    @Override
+    public Path getOutputPath() {
+      return archive;
+    }
+
+    private ZipOutputStream getStream(DiagnosticsHandler handler) {
+      assert !closed;
+      if (stream == null) {
+        try {
+          stream =
+              new ZipOutputStream(
+                  Files.newOutputStream(
+                      archive, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
+        } catch (IOException e) {
+          handler.error(new IOExceptionDiagnostic(e, origin));
+        }
+      }
+      return stream;
+    }
+
+    private synchronized void synchronizedWrite(
+        String entry, byte[] content, DiagnosticsHandler handler) {
+      try {
+        ZipUtils.writeToZipStream(getStream(handler), entry, content);
+      } catch (IOException e) {
+        handler.error(new IOExceptionDiagnostic(e, origin));
+      }
+    }
+
+    public static void writeResources(
+        Path archive,
+        List<ProgramResource> resources,
+        Map<Resource, String> primaryClassDescriptors)
+        throws IOException {
+      OpenOption[] options =
+          new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
+      try (Closer closer = Closer.create()) {
+        try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(archive, options))) {
+          for (ProgramResource resource : resources) {
+            String primaryClassDescriptor = primaryClassDescriptors.get(resource);
+            String entryName = getDexFileName(primaryClassDescriptor);
+            byte[] bytes = ByteStreams.toByteArray(closer.register(resource.getStream()));
+            ZipUtils.writeToZipStream(out, entryName, bytes);
+          }
+        }
+      }
+    }
+  }
+
+  /** Directory consumer to write program resources to a directory. */
+  class DirectoryConsumer extends ForwardingConsumer {
+
+    private final Path directory;
+
+    public DirectoryConsumer(Path directory) {
+      this(directory, null);
+    }
+
+    public DirectoryConsumer(Path directory, DexFilePerClassFileConsumer consumer) {
+      super(consumer);
+      this.directory = directory;
+    }
+
+    @Override
+    public void accept(
+        String primaryClassDescriptor,
+        byte[] data,
+        Set<String> descriptors,
+        DiagnosticsHandler handler) {
+      super.accept(primaryClassDescriptor, data, descriptors, handler);
+      Path target = getTargetDexFile(directory, primaryClassDescriptor);
+      try {
+        writeFile(data, target);
+      } catch (IOException e) {
+        handler.error(new IOExceptionDiagnostic(e, new PathOrigin(target)));
+      }
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      super.finished(handler);
+    }
+
+    @Override
+    public Path getOutputPath() {
+      return directory;
+    }
+
+    public static void writeResources(
+        Path directory,
+        List<ProgramResource> resources,
+        Map<Resource, String> primaryClassDescriptors)
+        throws IOException {
+      try (Closer closer = Closer.create()) {
+        for (ProgramResource resource : resources) {
+          String primaryClassDescriptor = primaryClassDescriptors.get(resource);
+          Path target = getTargetDexFile(directory, primaryClassDescriptor);
+          writeFile(ByteStreams.toByteArray(closer.register(resource.getStream())), target);
+        }
+      }
+    }
+
+    private static Path getTargetDexFile(Path directory, String primaryClassDescriptor) {
+      return directory.resolve(ArchiveConsumer.getDexFileName(primaryClassDescriptor));
+    }
+
+    private static void writeFile(byte[] contents, Path target) throws IOException {
+      Files.createDirectories(target.getParent());
+      FileUtils.writeToFile(target, null, contents);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/DexIndexedConsumer.java b/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
new file mode 100644
index 0000000..1bf16ce
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
@@ -0,0 +1,257 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.IOExceptionDiagnostic;
+import com.android.tools.r8.utils.ZipUtils;
+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.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Consumer for DEX encoded programs.
+ *
+ * <p>This consumer receives DEX file content using standard indexed-multidex for programs larger
+ * than a single DEX file. This is the default consumer for DEX programs.
+ */
+public interface DexIndexedConsumer extends ProgramConsumer {
+
+  /**
+   * Callback to receive DEX data for a compilation output.
+   *
+   * <p>This is the equivalent to writing out the files classes.dex, classes2.dex, etc., where
+   * fileIndex gives the current file count (with the first file having index zero).
+   *
+   * <p>There is no guaranteed order and files might be written concurrently.
+   *
+   * <p>The consumer is expected not to throw, but instead report any errors via the diagnostics
+   * {@param handler}. If an error is reported via {@param handler} and no exceptions are thrown,
+   * then the compiler guaranties to exit with an error.
+   *
+   * @param fileIndex Index of the DEX file for multi-dexing. Files are zero-indexed.
+   * @param data DEX encoded data.
+   * @param descriptors Class descriptors for all classes defined in the DEX data.
+   * @param handler Diagnostics handler for reporting.
+   */
+  void accept(int fileIndex, byte[] data, Set<String> descriptors, DiagnosticsHandler handler);
+
+  /** Empty consumer to request the production of the resource but ignore its value. */
+  static DexIndexedConsumer emptyConsumer() {
+    return ForwardingConsumer.EMPTY_CONSUMER;
+  }
+
+  /** Forwarding consumer to delegate to an optional existing consumer. */
+  class ForwardingConsumer implements DexIndexedConsumer {
+
+    private static final DexIndexedConsumer EMPTY_CONSUMER = new ForwardingConsumer(null);
+
+    private final DexIndexedConsumer consumer;
+
+    public ForwardingConsumer(DexIndexedConsumer consumer) {
+      this.consumer = consumer;
+    }
+
+    @Override
+    public void accept(
+        int fileIndex, byte[] data, Set<String> descriptors, DiagnosticsHandler handler) {
+      if (consumer != null) {
+        consumer.accept(fileIndex, data, descriptors, handler);
+      }
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      if (consumer != null) {
+        consumer.finished(handler);
+      }
+    }
+
+    @Override
+    public Path getOutputPath() {
+      return consumer == null ? null : consumer.getOutputPath();
+    }
+  }
+
+  /** Archive consumer to write program resources to a zip archive. */
+  class ArchiveConsumer extends ForwardingConsumer {
+
+    private static String getDexFileName(int fileIndex) {
+      return fileIndex == 0
+          ? "classes.dex"
+          : ("classes" + (fileIndex + 1) + FileUtils.DEX_EXTENSION);
+    }
+
+    private final Path archive;
+    private final Origin origin;
+    private ZipOutputStream stream = null;
+    private boolean closed = false;
+
+    public ArchiveConsumer(Path archive) {
+      this(archive, null);
+    }
+
+    public ArchiveConsumer(Path archive, DexIndexedConsumer consumer) {
+      super(consumer);
+      this.archive = archive;
+      origin = new PathOrigin(archive);
+    }
+
+    @Override
+    public void accept(
+        int fileIndex, byte[] data, Set<String> descriptors, DiagnosticsHandler handler) {
+      super.accept(fileIndex, data, descriptors, handler);
+      synchronizedWrite(getDexFileName(fileIndex), data, handler);
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      super.finished(handler);
+      assert !closed;
+      closed = true;
+      try {
+        if (stream != null) {
+          stream.close();
+          stream = null;
+        }
+      } catch (IOException e) {
+        handler.error(new IOExceptionDiagnostic(e, origin));
+      }
+    }
+
+    @Override
+    public Path getOutputPath() {
+      return archive;
+    }
+
+    private ZipOutputStream getStream(DiagnosticsHandler handler) {
+      assert !closed;
+      if (stream == null) {
+        try {
+          stream =
+              new ZipOutputStream(
+                  Files.newOutputStream(
+                      archive, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
+        } catch (IOException e) {
+          handler.error(new IOExceptionDiagnostic(e, origin));
+        }
+      }
+      return stream;
+    }
+
+    private synchronized void synchronizedWrite(
+        String entry, byte[] content, DiagnosticsHandler handler) {
+      try {
+        ZipUtils.writeToZipStream(getStream(handler), entry, content);
+      } catch (IOException e) {
+        handler.error(new IOExceptionDiagnostic(e, origin));
+      }
+    }
+
+    public static void writeResources(Path archive, List<ProgramResource> resources)
+        throws IOException {
+      OpenOption[] options =
+          new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
+      try (Closer closer = Closer.create()) {
+        try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(archive, options))) {
+          for (int i = 0; i < resources.size(); i++) {
+            ProgramResource resource = resources.get(i);
+            String entryName = getDexFileName(i);
+            byte[] bytes = ByteStreams.toByteArray(closer.register(resource.getStream()));
+            ZipUtils.writeToZipStream(out, entryName, bytes);
+          }
+        }
+      }
+    }
+  }
+
+  /** Directory consumer to write program resources to a directory. */
+  class DirectoryConsumer extends ForwardingConsumer {
+
+    private final Path directory;
+    private boolean preparedDirectory = false;
+
+    public DirectoryConsumer(Path directory) {
+      this(directory, null);
+    }
+
+    public DirectoryConsumer(Path directory, DexIndexedConsumer consumer) {
+      super(consumer);
+      this.directory = directory;
+    }
+
+    @Override
+    public void accept(
+        int fileIndex, byte[] data, Set<String> descriptors, DiagnosticsHandler handler) {
+      super.accept(fileIndex, data, descriptors, handler);
+      Path target = getTargetDexFile(directory, fileIndex);
+      try {
+        prepareDirectory();
+        writeFile(data, target);
+      } catch (IOException e) {
+        handler.error(new IOExceptionDiagnostic(e, new PathOrigin(target)));
+      }
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      super.finished(handler);
+    }
+
+    @Override
+    public Path getOutputPath() {
+      return directory;
+    }
+
+    private synchronized void prepareDirectory() throws IOException {
+      if (preparedDirectory) {
+        return;
+      }
+      preparedDirectory = true;
+      deleteClassesDexFiles(directory);
+    }
+
+    public static void deleteClassesDexFiles(Path directory) throws IOException {
+      try (Stream<Path> filesInDir = Files.list(directory)) {
+        for (Path path : filesInDir.collect(Collectors.toList())) {
+          if (FileUtils.isClassesDexFile(path)) {
+            Files.delete(path);
+          }
+        }
+      }
+    }
+
+    public static void writeResources(Path directory, List<ProgramResource> resources)
+        throws IOException {
+      deleteClassesDexFiles(directory);
+      try (Closer closer = Closer.create()) {
+        for (int i = 0; i < resources.size(); i++) {
+          Resource resource = resources.get(i);
+          Path target = getTargetDexFile(directory, i);
+          writeFile(ByteStreams.toByteArray(closer.register(resource.getStream())), target);
+        }
+      }
+    }
+
+    private static Path getTargetDexFile(Path directory, int fileIndex) {
+      return directory.resolve(ArchiveConsumer.getDexFileName(fileIndex));
+    }
+
+    private static void writeFile(byte[] contents, Path target) throws IOException {
+      Files.createDirectories(target.getParent());
+      FileUtils.writeToFile(target, null, contents);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/OutputSink.java b/src/main/java/com/android/tools/r8/OutputSink.java
deleted file mode 100644
index a8d934a..0000000
--- a/src/main/java/com/android/tools/r8/OutputSink.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8;
-
-import java.io.IOException;
-import java.util.Set;
-
-/**
- * Interface used by D8 and R8 to output the generated results.
- * <p>
- * Implementations must be able to cope with concurrent calls to these methods and non-determinism
- * in the order of calls. For example, {@link #writeDexFile} may be called concurrently and in any
- * order. It is the responsibility of an implementation to ensure deterministic output, if such is
- * desired.
- * <p>
- * The two versions of {@link #writeDexFile} are not used simulatneously. Normally, D8 and R8 will
- * write output using the {@link #writeDexFile(byte[], Set, int)} method. Only if instructed to
- * generated a DEX file per class ({@link com.android.tools.r8.utils.OutputMode#FilePerInputClass})
- * will D8 invoke {@link #writeDexFile(byte[], Set, String)} to generate a corresponding DEX file.
- * <p>
- * See {@link com.android.tools.r8.utils.ForwardingOutputSink} for a helper class that can be used
- * to wrap an existing sink and override only certain behavior.
- */
-public interface OutputSink {
-
-  /**
-   * Write a DEX file containing the definitions for all classes in classDescriptors into the DEX
-   * file numbered as fileId.
-   * <p>
-   * This is the equivalent to writing out the files classes.dex, classes2.dex, etc., where fileId
-   * gives the current file count.
-   * <p>
-   * Files are not necessarily generated in order and files might be written concurrently. However,
-   * for each fileId only one file is ever written. If this method is called, the other writeDexFile
-   * and writeClassFile methods will not be called.
-   */
-  void writeDexFile(byte[] contents, Set<String> classDescriptors, int fileId) throws IOException;
-
-  /**
-   * Write a DEX file that contains the class primaryClassName and its companion classes.
-   * <p>
-   * This is equivalent to writing out the file com/foo/bar/Test.dex given a primaryClassName of
-   * com.foo.bar.Test.
-   * <p>
-   * There is no guaranteed order and files might be written concurrently. However, for each
-   * primaryClassName only one file is ever written.
-   * <p>
-   * This method is only invoked by D8 and only if compiling each class into its own dex file, e.g.,
-   * for incremental compilation. If this method is called, the other writeDexFile and
-   * writeClassFile methods will not be called.
-   */
-  void writeDexFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
-      throws IOException;
-
-  /**
-   * Closes the output sink.
-   * <p>
-   * This method is invokes once all output has been generated.
-   */
-  void close() throws IOException;
-}
diff --git a/src/main/java/com/android/tools/r8/ProgramConsumer.java b/src/main/java/com/android/tools/r8/ProgramConsumer.java
index 061741c..fda6390 100644
--- a/src/main/java/com/android/tools/r8/ProgramConsumer.java
+++ b/src/main/java/com/android/tools/r8/ProgramConsumer.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import java.nio.file.Path;
+
 /**
  * Base for all program consumers to allow abstracting which concrete consumer is provided to D8/R8.
  */
@@ -16,4 +18,9 @@
    * @param handler Diagnostics handler for reporting.
    */
   void finished(DiagnosticsHandler handler);
+
+  @Deprecated
+  default Path getOutputPath() {
+    return null;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 3e30ebf..7da70d4 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -47,7 +47,7 @@
 import com.android.tools.r8.utils.AbortException;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.AndroidAppOutputSink;
+import com.android.tools.r8.utils.AndroidAppConsumers;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.IOExceptionDiagnostic;
@@ -103,7 +103,6 @@
   public static void writeApplication(
       ExecutorService executorService,
       DexApplication application,
-      OutputSink outputSink,
       String deadCode,
       NamingLens namingLens,
       String proguardSeedsData,
@@ -124,7 +123,7 @@
                 namingLens,
                 proguardSeedsData,
                 proguardMapSupplier)
-            .write(outputSink, executorService);
+            .write(executorService);
       }
     } catch (IOException e) {
       throw new RuntimeException("Cannot write application", e);
@@ -138,11 +137,11 @@
     return result;
   }
 
-  static void runForTesting(AndroidApp app, OutputSink outputSink, InternalOptions options)
+  static void runForTesting(AndroidApp app, InternalOptions options)
       throws IOException, CompilationException {
     ExecutorService executor = ThreadUtils.getExecutorService(options);
     try {
-      run(app, outputSink, options, executor);
+      run(app, options, executor);
     } finally {
       executor.shutdown();
     }
@@ -150,15 +149,15 @@
 
   private static void run(
       AndroidApp app,
-      OutputSink outputSink,
       InternalOptions options,
       ExecutorService executor)
       throws IOException, CompilationException {
-    new R8(options).run(app, outputSink, executor);
+    new R8(options).run(app, executor);
   }
 
-  private void run(AndroidApp inputApp, OutputSink outputSink, ExecutorService executorService)
+  private void run(AndroidApp inputApp, ExecutorService executorService)
       throws IOException, CompilationException {
+    assert options.programConsumer != null;
     if (options.quiet) {
       System.setOut(new PrintStream(ByteStreams.nullOutputStream()));
     }
@@ -380,7 +379,6 @@
       writeApplication(
           executorService,
           application,
-          outputSink,
           application.deadCode,
           namingLens,
           proguardSeedsData,
@@ -392,8 +390,7 @@
       unwrapExecutionException(e);
       throw new AssertionError(e); // unwrapping method should have thrown
     } finally {
-      outputSink.close();
-      options.closeProgramConsumer();
+      options.signalFinishedToProgramConsumer();
       // Dump timings.
       if (options.printTimes) {
         timing.report();
@@ -477,7 +474,7 @@
     try {
       try {
         InternalOptions options = command.getInternalOptions();
-        run(command.getInputApp(), command.getOutputSink(), options, executor);
+        run(command.getInputApp(), options, executor);
       } catch (IOException io) {
         throw command.getReporter().fatalError(new IOExceptionDiagnostic(io));
       } catch (CompilationException e) {
@@ -495,9 +492,9 @@
   public static AndroidApp runInternal(R8Command command, ExecutorService executor)
       throws IOException, CompilationException {
     InternalOptions options = command.getInternalOptions();
-    AndroidAppOutputSink compatSink = new AndroidAppOutputSink(command.getOutputSink(), options);
-    run(command.getInputApp(), compatSink, options, executor);
-    return compatSink.build();
+    AndroidAppConsumers compatConsumers = new AndroidAppConsumers(options);
+    run(command.getInputApp(), options, executor);
+    return compatConsumers.build();
   }
 
   private static void run(String[] args)
@@ -518,7 +515,7 @@
     InternalOptions options = command.getInternalOptions();
     ExecutorService executorService = ThreadUtils.getExecutorService(options);
     try {
-      run(command.getInputApp(), command.getOutputSink(), options, executorService);
+      run(command.getInputApp(), options, executorService);
     } finally {
       executorService.shutdown();
     }
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index dd8d0ff..065dbc8 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -280,8 +280,7 @@
       R8Command command =
           new R8Command(
               getAppBuilder().build(),
-              getOutputPath(),
-              getOutputMode(),
+              getOutputPath() == null ? null : getProgramConsumer(),
               mainDexKeepRules,
               mainDexListConsumer,
               configuration,
@@ -351,7 +350,6 @@
     return new Builder(diagnosticsHandler);
   }
 
-
   // Internal builder to start from an existing AndroidApp.
   static Builder builder(AndroidApp app) {
     return new Builder(app);
@@ -461,8 +459,7 @@
 
   private R8Command(
       AndroidApp inputApp,
-      Path outputPath,
-      OutputMode outputMode,
+      ProgramConsumer programConsumer,
       ImmutableList<ProguardConfigurationRule> mainDexKeepRules,
       StringConsumer mainDexListConsumer,
       ProguardConfiguration proguardConfiguration,
@@ -478,8 +475,7 @@
       boolean ignoreMissingClassesWhenNotShrinking,
       StringConsumer proguardMapConsumer,
       Path proguardCompatibilityRulesOutput) {
-    super(inputApp, outputPath, outputMode, mode, minApiLevel, reporter,
-        enableDesugaring);
+    super(inputApp, mode, programConsumer, minApiLevel, reporter, enableDesugaring);
     assert proguardConfiguration != null;
     assert mainDexKeepRules != null;
     assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
@@ -510,6 +506,7 @@
     proguardMapConsumer = null;
     proguardCompatibilityRulesOutput = null;
   }
+
   public boolean useTreeShaking() {
     return useTreeShaking;
   }
@@ -527,6 +524,7 @@
     InternalOptions internal = new InternalOptions(proguardConfiguration, getReporter());
     assert !internal.debug;
     internal.debug = getMode() == CompilationMode.DEBUG;
+    internal.programConsumer = getProgramConsumer();
     internal.minApiLevel = getMinApiLevel();
     // -dontoptimize disables optimizations by flipping related flags.
     if (!proguardConfiguration.isOptimizing()) {
@@ -554,7 +552,6 @@
     internal.minimalMainDex = internal.debug;
     internal.mainDexListConsumer = mainDexListConsumer;
 
-    internal.outputMode = getOutputMode();
     if (internal.debug) {
       // TODO(zerny): Should we support removeSwitchMaps in debug mode? b/62936642
       internal.removeSwitchMaps = false;
diff --git a/src/main/java/com/android/tools/r8/R8Output.java b/src/main/java/com/android/tools/r8/R8Output.java
deleted file mode 100644
index 5134a39..0000000
--- a/src/main/java/com/android/tools/r8/R8Output.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8;
-
-import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.OutputMode;
-import java.io.IOException;
-import java.nio.file.Path;
-
-/** Represents the output of a R8 compilation. */
-public class R8Output extends BaseOutput {
-
-  R8Output(AndroidApp app, OutputMode outputMode) {
-    super(app, outputMode);
-  }
-
-  @Override
-  public void write(Path output) throws IOException {
-    getAndroidApp().write(output, getOutputMode());
-  }
-}
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 570d773..4ea47a2 100644
--- a/src/main/java/com/android/tools/r8/bisect/Bisect.java
+++ b/src/main/java/com/android/tools/r8/bisect/Bisect.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.AndroidAppOutputSink;
+import com.android.tools.r8.utils.AndroidAppConsumers;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.Timing;
@@ -177,9 +177,9 @@
   private static void writeApp(DexApplication app, Path output, ExecutorService executor)
       throws IOException, ExecutionException, DexOverflowException {
     InternalOptions options = new InternalOptions();
-    AndroidAppOutputSink compatSink = new AndroidAppOutputSink();
+    AndroidAppConsumers compatSink = new AndroidAppConsumers(options);
     ApplicationWriter writer = new ApplicationWriter(app, options, null, null, null, null, null);
-    writer.write(compatSink, executor);
+    writer.write(executor);
     compatSink.build().writeToDirectory(output, OutputMode.Indexed);
   }
 
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 0d92201..c9580ae 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -4,7 +4,6 @@
 package com.android.tools.r8.dex;
 
 import com.android.tools.r8.ApiLevelException;
-import com.android.tools.r8.OutputSink;
 import com.android.tools.r8.errors.DexOverflowException;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationDirectory;
@@ -27,7 +26,6 @@
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.google.common.collect.ObjectArrays;
 import java.io.IOException;
@@ -132,7 +130,7 @@
       throws ExecutionException, IOException, DexOverflowException {
     // Distribute classes into dex files.
     VirtualFile.Distributor distributor;
-    if (options.outputMode == OutputMode.FilePerInputClass) {
+    if (options.isGeneratingDexFilePerClassFile()) {
       distributor = new VirtualFile.FilePerInputClassDistributor(this);
     } else if (!options.canUseMultidex()
         && options.mainDexKeepRules.isEmpty()
@@ -145,7 +143,7 @@
     return distributor.run();
   }
 
-  public void write(OutputSink outputSink, ExecutorService executorService)
+  public void write(ExecutorService executorService)
       throws IOException, ExecutionException, DexOverflowException {
     application.timing.begin("DexApplication.write");
     try {
@@ -183,19 +181,29 @@
         for (VirtualFile virtualFile : offsetMappingFutures.keySet()) {
           assert !virtualFile.isEmpty();
           final ObjectToOffsetMapping mapping = offsetMappingFutures.get(virtualFile).get();
-          dexDataFutures.add(executorService.submit(() -> {
-            byte[] result = writeDexFile(mapping);
-            if (virtualFile.getPrimaryClassDescriptor() != null) {
-              outputSink.writeDexFile(
-                  result,
-                  virtualFile.getClassDescriptors(),
-                  virtualFile.getPrimaryClassDescriptor());
-            } else {
-              outputSink
-                  .writeDexFile(result, virtualFile.getClassDescriptors(), virtualFile.getId());
-            }
-            return true;
-          }));
+          dexDataFutures.add(
+              executorService.submit(
+                  () -> {
+                    byte[] result = writeDexFile(mapping);
+                    if (virtualFile.getPrimaryClassDescriptor() != null) {
+                      options
+                          .getDexFilePerClassFileConsumer()
+                          .accept(
+                              virtualFile.getPrimaryClassDescriptor(),
+                              result,
+                              virtualFile.getClassDescriptors(),
+                              options.reporter);
+                    } else {
+                      options
+                          .getDexIndexedConsumer()
+                          .accept(
+                              virtualFile.getId(),
+                              result,
+                              virtualFile.getClassDescriptors(),
+                              options.reporter);
+                    }
+                    return true;
+                  }));
         }
       } catch (InterruptedException e) {
         throw new RuntimeException("Interrupted while waiting for future.", e);
@@ -205,6 +213,8 @@
       offsetMappingFutures.clear();
       // Wait for all files to be processed before moving on.
       ThreadUtils.awaitFutures(dexDataFutures);
+      // Fail if there are pending errors, e.g., the program consumers may have reported errors.
+      options.reporter.failIfPendingErrors();
 
       if (options.usageInformationConsumer != null && deadCode != null) {
         ExceptionUtils.withConsumeResourceHandler(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 9004a44..a5d3c66 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -94,6 +94,7 @@
       boolean enableWholeProgramOptimizations) {
     assert appInfo != null;
     assert options != null;
+    assert options.programConsumer != null;
     this.timing = timing != null ? timing : new Timing("internal");
     this.appInfo = appInfo;
     this.graphLense = graphLense != null ? graphLense : GraphLense.getIdentityLense();
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 5302f51..7703142 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -9,11 +9,12 @@
 
 import com.android.tools.r8.ArchiveClassFileProvider;
 import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.ProgramResource.Kind;
 import com.android.tools.r8.Resource;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.shaking.FilteredClassPath;
@@ -27,13 +28,9 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
-import java.nio.file.CopyOption;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
-import java.nio.file.OpenOption;
 import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -42,9 +39,6 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
 
 /**
  * Collection of program files needed for processing.
@@ -303,38 +297,12 @@
    * Write the dex program resources and proguard resource to @code{directory}.
    */
   public void writeToDirectory(Path directory, OutputMode outputMode) throws IOException {
+    List<ProgramResource> dexProgramSources = getDexProgramResources();
     if (outputMode == OutputMode.Indexed) {
-      try (Stream<Path> filesInDir = Files.list(directory)) {
-        for (Path path : filesInDir.collect(Collectors.toList())) {
-          if (FileUtils.isClassesDexFile(path)) {
-            Files.delete(path);
-          }
-        }
-      }
-    }
-    CopyOption[] options = new CopyOption[] {StandardCopyOption.REPLACE_EXISTING};
-    try (Closer closer = Closer.create()) {
-      List<ProgramResource> dexProgramSources = getDexProgramResources();
-      for (int i = 0; i < dexProgramSources.size(); i++) {
-        Path filePath = directory.resolve(getOutputPath(outputMode, dexProgramSources.get(i), i));
-        if (!Files.exists(filePath.getParent())) {
-          Files.createDirectories(filePath.getParent());
-        }
-        Files.copy(closer.register(dexProgramSources.get(i).getStream()), filePath, options);
-      }
-    }
-  }
-
-  private String getOutputPath(OutputMode outputMode, Resource resource, int index) {
-    switch (outputMode) {
-      case Indexed:
-        return index == 0 ? "classes.dex" : ("classes" + (index + 1) + ".dex");
-      case FilePerInputClass:
-        String classDescriptor = programResourcesMainDescriptor.get(resource);
-        assert classDescriptor!= null && DescriptorUtils.isClassDescriptor(classDescriptor);
-        return classDescriptor.substring(1, classDescriptor.length() - 1) + ".dex";
-      default:
-        throw new Unreachable("Unknown output mode: " + outputMode);
+      DexIndexedConsumer.DirectoryConsumer.writeResources(directory, dexProgramSources);
+    } else {
+      DexFilePerClassFileConsumer.DirectoryConsumer.writeResources(
+          directory, dexProgramSources, programResourcesMainDescriptor);
     }
   }
 
@@ -356,21 +324,12 @@
    * Write the dex program resources to @code{archive} and the proguard resource as its sibling.
    */
   public void writeToZip(Path archive, OutputMode outputMode) throws IOException {
-    OpenOption[] options =
-        new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
-    try (Closer closer = Closer.create()) {
-      try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(archive, options))) {
-        List<ProgramResource> dexProgramSources = getDexProgramResources();
-        for (int i = 0; i < dexProgramSources.size(); i++) {
-          ZipEntry zipEntry = new ZipEntry(getOutputPath(outputMode, dexProgramSources.get(i), i));
-          byte[] bytes =
-              ByteStreams.toByteArray(closer.register(dexProgramSources.get(i).getStream()));
-          zipEntry.setSize(bytes.length);
-          out.putNextEntry(zipEntry);
-          out.write(bytes);
-          out.closeEntry();
-        }
-      }
+    List<ProgramResource> resources = getDexProgramResources();
+    if (outputMode == OutputMode.Indexed) {
+      DexIndexedConsumer.ArchiveConsumer.writeResources(archive, resources);
+    } else {
+      DexFilePerClassFileConsumer.ArchiveConsumer.writeResources(
+          archive, resources, programResourcesMainDescriptor);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java b/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java
new file mode 100644
index 0000000..b0eb8e6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java
@@ -0,0 +1,203 @@
+// 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.ClassFileConsumer;
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DexIndexedConsumer.ForwardingConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.origin.Origin;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeMap;
+
+public class AndroidAppConsumers {
+
+  private final AndroidApp.Builder builder = AndroidApp.builder();
+  private boolean closed = false;
+
+  private ProgramConsumer programConsumer = null;
+  private StringConsumer mainDexListConsumer = null;
+  private StringConsumer proguardMapConsumer = null;
+  private StringConsumer usageInformationConsumer = null;
+
+  public AndroidAppConsumers(InternalOptions options) {
+    options.programConsumer = wrapProgramConsumer(options.programConsumer);
+    options.mainDexListConsumer = wrapMainDexListConsumer(options.mainDexListConsumer);
+    options.proguardMapConsumer = wrapProguardMapConsumer(options.proguardMapConsumer);
+    options.usageInformationConsumer =
+        wrapUsageInformationConsumer(options.usageInformationConsumer);
+  }
+
+  private ProgramConsumer wrapProgramConsumer(ProgramConsumer consumer) {
+    assert programConsumer == null;
+    if (consumer instanceof ClassFileConsumer) {
+      programConsumer = wrapClassFileConsumer((ClassFileConsumer) consumer);
+    } else if (consumer instanceof DexIndexedConsumer) {
+      programConsumer = wrapDexIndexedConsumer((DexIndexedConsumer) consumer);
+    } else if (consumer instanceof DexFilePerClassFileConsumer) {
+      programConsumer = wrapDexFilePerClassFileConsumer((DexFilePerClassFileConsumer) consumer);
+    } else {
+      // TODO(zerny): Refine API to disallow running without a program consumer.
+      assert consumer == null;
+      programConsumer = wrapDexIndexedConsumer(null);
+    }
+    return programConsumer;
+  }
+
+  private StringConsumer wrapMainDexListConsumer(StringConsumer consumer) {
+    assert mainDexListConsumer == null;
+    if (consumer != null) {
+      mainDexListConsumer =
+          new StringConsumer.ForwardingConsumer(consumer) {
+            @Override
+            public void accept(String string, DiagnosticsHandler handler) {
+              super.accept(string, handler);
+              builder.setMainDexListOutputData(string.getBytes(StandardCharsets.UTF_8));
+            }
+          };
+    }
+    return mainDexListConsumer;
+  }
+
+  private StringConsumer wrapProguardMapConsumer(StringConsumer consumer) {
+    assert proguardMapConsumer == null;
+    if (consumer != null) {
+      proguardMapConsumer =
+          new StringConsumer.ForwardingConsumer(consumer) {
+            @Override
+            public void accept(String string, DiagnosticsHandler handler) {
+              super.accept(string, handler);
+              builder.setProguardMapData(string);
+            }
+          };
+    }
+    return proguardMapConsumer;
+  }
+
+  private StringConsumer wrapUsageInformationConsumer(StringConsumer consumer) {
+    assert usageInformationConsumer == null;
+    if (consumer != null) {
+      usageInformationConsumer =
+          new StringConsumer.ForwardingConsumer(consumer) {
+            @Override
+            public void accept(String string, DiagnosticsHandler handler) {
+              super.accept(string, handler);
+              builder.setDeadCode(string.getBytes(StandardCharsets.UTF_8));
+            }
+          };
+    }
+    return usageInformationConsumer;
+  }
+
+  private DexIndexedConsumer wrapDexIndexedConsumer(DexIndexedConsumer consumer) {
+    return new ForwardingConsumer(consumer) {
+
+      // Sort the files by id so that their order is deterministic. Some tests depend on this.
+      private Int2ReferenceSortedMap<DescriptorsWithContents> files =
+          new Int2ReferenceAVLTreeMap<>();
+
+      @Override
+      public void accept(
+          int fileIndex, byte[] data, Set<String> descriptors, DiagnosticsHandler handler) {
+        super.accept(fileIndex, data, descriptors, handler);
+        addDexFile(fileIndex, data, descriptors);
+      }
+
+      @Override
+      public void finished(DiagnosticsHandler handler) {
+        super.finished(handler);
+        closed = true;
+        files.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors));
+        files = null;
+      }
+
+      synchronized void addDexFile(int fileIndex, byte[] data, Set<String> descriptors) {
+        files.put(fileIndex, new DescriptorsWithContents(descriptors, data));
+      }
+    };
+  }
+
+  private DexFilePerClassFileConsumer wrapDexFilePerClassFileConsumer(
+      DexFilePerClassFileConsumer consumer) {
+    return new DexFilePerClassFileConsumer.ForwardingConsumer(consumer) {
+
+      // Sort the files by their name for good measure.
+      private TreeMap<String, DescriptorsWithContents> files = new TreeMap<>();
+
+      @Override
+      public void accept(
+          String primaryClassDescriptor,
+          byte[] data,
+          Set<String> descriptors,
+          DiagnosticsHandler handler) {
+        super.accept(primaryClassDescriptor, data, descriptors, handler);
+        addDexFile(primaryClassDescriptor, data, descriptors);
+      }
+
+      synchronized void addDexFile(
+          String primaryClassDescriptor, byte[] data, Set<String> descriptors) {
+        files.put(primaryClassDescriptor, new DescriptorsWithContents(descriptors, data));
+      }
+
+      @Override
+      public void finished(DiagnosticsHandler handler) {
+        super.finished(handler);
+        closed = true;
+        files.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors, v));
+        files = null;
+      }
+    };
+  }
+
+  private ClassFileConsumer wrapClassFileConsumer(ClassFileConsumer consumer) {
+    return new ClassFileConsumer.ForwardingConsumer(consumer) {
+
+      private List<DescriptorsWithContents> files = new ArrayList<>();
+
+      @Override
+      public void accept(byte[] data, String descriptor, DiagnosticsHandler handler) {
+        super.accept(data, descriptor, handler);
+        addClassFile(data, descriptor);
+      }
+
+      synchronized void addClassFile(byte[] data, String descriptor) {
+        files.add(new DescriptorsWithContents(Collections.singleton(descriptor), data));
+      }
+
+      @Override
+      public void finished(DiagnosticsHandler handler) {
+        super.finished(handler);
+        closed = true;
+        files.forEach(
+            d -> builder.addClassProgramData(d.contents, Origin.unknown(), d.descriptors));
+        files = null;
+      }
+    };
+  }
+
+  public AndroidApp build() {
+    assert closed;
+    return builder.build();
+  }
+
+  private static class DescriptorsWithContents {
+
+    final Set<String> descriptors;
+    final byte[] contents;
+
+    private DescriptorsWithContents(Set<String> descriptors, byte[] contents) {
+      this.descriptors = descriptors;
+      this.contents = contents;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java b/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java
deleted file mode 100644
index ee771ff..0000000
--- a/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java
+++ /dev/null
@@ -1,139 +0,0 @@
-// 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.DiagnosticsHandler;
-import com.android.tools.r8.OutputSink;
-import com.android.tools.r8.StringConsumer;
-import com.android.tools.r8.origin.Origin;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.TreeMap;
-
-public class AndroidAppOutputSink extends ForwardingOutputSink {
-
-  private final AndroidApp.Builder builder = AndroidApp.builder();
-  private final TreeMap<String, DescriptorsWithContents> dexFilesWithPrimary = new TreeMap<>();
-  private final TreeMap<Integer, DescriptorsWithContents> dexFilesWithId = new TreeMap<>();
-  private final List<DescriptorsWithContents> classFiles = new ArrayList<>();
-  private boolean closed = false;
-
-  private StringConsumer mainDexListConsumer = null;
-  private StringConsumer proguardMapConsumer = null;
-  private StringConsumer usageInformationConsumer = null;
-
-  public AndroidAppOutputSink(OutputSink forwardTo, InternalOptions options) {
-    super(forwardTo);
-    options.mainDexListConsumer = wrapMainDexListConsumer(options.mainDexListConsumer);
-    options.proguardMapConsumer = wrapProguardMapConsumer(options.proguardMapConsumer);
-    options.usageInformationConsumer =
-        wrapUsageInformationConsumer(options.usageInformationConsumer);
-  }
-
-  public AndroidAppOutputSink() {
-    super(new IgnoreContentsOutputSink());
-  }
-
-  private StringConsumer wrapMainDexListConsumer(StringConsumer consumer) {
-    assert mainDexListConsumer == null;
-    if (consumer != null) {
-      mainDexListConsumer =
-          new StringConsumer.ForwardingConsumer(consumer) {
-            @Override
-            public void accept(String string, DiagnosticsHandler handler) {
-              super.accept(string, handler);
-              builder.setMainDexListOutputData(string.getBytes(StandardCharsets.UTF_8));
-            }
-          };
-    }
-    return mainDexListConsumer;
-  }
-
-  private StringConsumer wrapProguardMapConsumer(StringConsumer consumer) {
-    assert proguardMapConsumer == null;
-    if (consumer != null) {
-      proguardMapConsumer =
-          new StringConsumer.ForwardingConsumer(consumer) {
-            @Override
-            public void accept(String string, DiagnosticsHandler handler) {
-              super.accept(string, handler);
-              builder.setProguardMapData(string);
-            }
-          };
-    }
-    return proguardMapConsumer;
-  }
-
-  private StringConsumer wrapUsageInformationConsumer(StringConsumer consumer) {
-    assert usageInformationConsumer == null;
-    if (consumer != null) {
-      usageInformationConsumer = new StringConsumer.ForwardingConsumer(consumer) {
-        @Override
-        public void accept(String string, DiagnosticsHandler handler) {
-          super.accept(string, handler);
-          builder.setDeadCode(string.getBytes(StandardCharsets.UTF_8));
-        }
-      };
-    }
-    return usageInformationConsumer;
-  }
-
-  @Override
-  public synchronized void writeDexFile(byte[] contents, Set<String> classDescriptors, int fileId)
-      throws IOException {
-    assert dexFilesWithPrimary.isEmpty() && classFiles.isEmpty();
-    // Sort the files by id so that their order is deterministic. Some tests depend on this.
-    dexFilesWithId.put(fileId, new DescriptorsWithContents(classDescriptors, contents));
-    super.writeDexFile(contents, classDescriptors, fileId);
-  }
-
-  @Override
-  public synchronized void writeDexFile(byte[] contents, Set<String> classDescriptors,
-      String primaryClassName)
-      throws IOException {
-    assert dexFilesWithId.isEmpty() && classFiles.isEmpty();
-    // Sort the files by their name for good measure.
-    dexFilesWithPrimary
-        .put(primaryClassName, new DescriptorsWithContents(classDescriptors, contents));
-    super.writeDexFile(contents, classDescriptors, primaryClassName);
-  }
-
-  @Override
-  public void close() throws IOException {
-    assert !closed;
-    if (!dexFilesWithPrimary.isEmpty()) {
-      assert dexFilesWithId.isEmpty() && classFiles.isEmpty();
-      dexFilesWithPrimary.forEach(
-          (v, d) -> builder.addDexProgramData(d.contents, d.descriptors, v));
-    } else if (!dexFilesWithId.isEmpty()) {
-      assert dexFilesWithPrimary.isEmpty() && classFiles.isEmpty();
-      dexFilesWithId.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors));
-    } else if (!classFiles.isEmpty()) {
-      assert dexFilesWithPrimary.isEmpty() && dexFilesWithId.isEmpty();
-      classFiles.forEach(
-          d -> builder.addClassProgramData(d.contents, Origin.unknown(), d.descriptors));
-    }
-    closed = true;
-    super.close();
-  }
-
-  public AndroidApp build() {
-    assert closed;
-    return builder.build();
-  }
-
-  private static class DescriptorsWithContents {
-
-    final Set<String> descriptors;
-    final byte[] contents;
-
-    private DescriptorsWithContents(Set<String> descriptors, byte[] contents) {
-      this.descriptors = descriptors;
-      this.contents = contents;
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java b/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java
deleted file mode 100644
index 74b1b21..0000000
--- a/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// 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 static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-public class DirectoryOutputSink extends FileSystemOutputSink {
-
-  private final Path outputDirectory;
-
-  public DirectoryOutputSink(Path outputDirectory, InternalOptions options) throws IOException {
-    super(options);
-    this.outputDirectory = outputDirectory;
-    cleanUpOutputDirectory();
-  }
-
-  private void cleanUpOutputDirectory() throws IOException {
-    if (getOutputMode() == OutputMode.Indexed) {
-      try (Stream<Path> filesInDir = Files.list(outputDirectory)) {
-        for (Path path : filesInDir.collect(Collectors.toList())) {
-          if (FileUtils.isClassesDexFile(path)) {
-            Files.delete(path);
-          }
-        }
-      }
-    }
-  }
-
-  @Override
-  public void writeDexFile(byte[] contents, Set<String> classDescriptors, int fileId)
-      throws IOException {
-    Path target = outputDirectory.resolve(getOutputFileName(fileId));
-    Files.createDirectories(target.getParent());
-    FileUtils.writeToFile(target, null, contents);
-  }
-
-  @Override
-  public void writeDexFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
-      throws IOException {
-    writeFileFromDescriptor(contents, primaryClassName, DEX_EXTENSION);
-  }
-
-  private void writeFileFromDescriptor(byte[] contents, String descriptor, String extension)
-      throws IOException {
-    Path target = outputDirectory.resolve(getOutputFileName(descriptor, extension));
-    Files.createDirectories(target.getParent());
-    FileUtils.writeToFile(target, null, contents);
-  }
-
-  @Override
-  public void close() throws IOException {
-    // Intentionally left empty.
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java b/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java
deleted file mode 100644
index b2699fc..0000000
--- a/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// 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.OutputSink;
-import java.io.IOException;
-import java.nio.file.Path;
-
-public abstract class FileSystemOutputSink implements OutputSink {
-
-  private final InternalOptions options;
-
-  protected FileSystemOutputSink(InternalOptions options) {
-    this.options = options;
-  }
-
-  public static FileSystemOutputSink create(Path outputPath, InternalOptions options)
-      throws IOException {
-    if (FileUtils.isArchive(outputPath)) {
-      return new ZipFileOutputSink(outputPath, options);
-    } else {
-      return new DirectoryOutputSink(outputPath, options);
-    }
-  }
-
-  String getOutputFileName(int index) {
-    return index == 0 ? "classes.dex" : ("classes" + (index + 1) + FileUtils.DEX_EXTENSION);
-  }
-
-  String getOutputFileName(String classDescriptor, String extension) throws IOException {
-    assert classDescriptor != null && DescriptorUtils.isClassDescriptor(classDescriptor);
-    return DescriptorUtils.getClassBinaryNameFromDescriptor(classDescriptor) + extension;
-  }
-
-  protected OutputMode getOutputMode() {
-    return options.outputMode;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index 580b7b7..23db21b 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -126,7 +126,7 @@
     return mapOut;
   }
 
-  static boolean isClassesDexFile(Path file) {
+  public static boolean isClassesDexFile(Path file) {
     String name = file.getFileName().toString().toLowerCase();
     if (!name.startsWith("classes") || !name.endsWith(DEX_EXTENSION)) {
       return false;
diff --git a/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java b/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java
deleted file mode 100644
index 71e48cf..0000000
--- a/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// 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.OutputSink;
-import java.io.IOException;
-import java.util.Set;
-
-/**
- * Implementation of an {@link OutputSink} that forwards all calls to another sink.
- * <p>
- * Useful for layering output sinks and intercept some output.
- */
-public abstract class ForwardingOutputSink implements OutputSink {
-
-  private final OutputSink forwardTo;
-
-  protected ForwardingOutputSink(OutputSink forwardTo) {
-    this.forwardTo = forwardTo;
-  }
-
-  @Override
-  public void writeDexFile(byte[] contents, Set<String> classDescriptors, int fileId)
-      throws IOException {
-    forwardTo.writeDexFile(contents, classDescriptors, fileId);
-  }
-
-  @Override
-  public void writeDexFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
-      throws IOException {
-    forwardTo.writeDexFile(contents, classDescriptors, primaryClassName);
-  }
-
-  @Override
-  public void close() throws IOException {
-    forwardTo.close();
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java b/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java
deleted file mode 100644
index 76f03bd..0000000
--- a/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// 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.OutputSink;
-import java.io.IOException;
-import java.util.Set;
-
-public class IgnoreContentsOutputSink implements OutputSink {
-
-  @Override
-  public void writeDexFile(byte[] contents, Set<String> classDescriptors, int fileId) {
-    // Intentionally left empty.
-  }
-
-  @Override
-  public void writeDexFile(byte[] contents, Set<String> classDescriptors, String primaryClassName) {
-    // Intentionally left empty.
-  }
-
-  @Override
-  public void close() throws IOException {
-    // Intentionally left empty.
-  }
-}
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 9ae315f..7c97839 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -4,6 +4,8 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.ProgramConsumer;
 import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.dex.Marker;
@@ -103,18 +105,34 @@
   }
 
   public boolean isGeneratingDex() {
-    return programConsumer == null;
+    return isGeneratingDexIndexed() || isGeneratingDexFilePerClassFile();
+  }
+
+  public boolean isGeneratingDexIndexed() {
+    return programConsumer instanceof DexIndexedConsumer;
+  }
+
+  public boolean isGeneratingDexFilePerClassFile() {
+    return programConsumer instanceof DexFilePerClassFileConsumer;
   }
 
   public boolean isGeneratingClassFiles() {
     return programConsumer instanceof ClassFileConsumer;
   }
 
+  public DexIndexedConsumer getDexIndexedConsumer() {
+    return (DexIndexedConsumer) programConsumer;
+  }
+
+  public DexFilePerClassFileConsumer getDexFilePerClassFileConsumer() {
+    return (DexFilePerClassFileConsumer) programConsumer;
+  }
+
   public ClassFileConsumer getClassFileConsumer() {
     return (ClassFileConsumer) programConsumer;
   }
 
-  public void closeProgramConsumer() {
+  public void signalFinishedToProgramConsumer() {
     if (programConsumer != null) {
       programConsumer.finished(reporter);
     }
@@ -133,9 +151,6 @@
   // Defines try-with-resources rewriter behavior.
   public OffOrAuto tryWithResourcesDesugaring = OffOrAuto.Auto;
 
-  // Application writing mode.
-  public OutputMode outputMode = OutputMode.Indexed;
-
   public boolean useTreeShaking = true;
   public boolean useDiscardedChecker = true;
 
diff --git a/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java b/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java
deleted file mode 100644
index 1e1037e..0000000
--- a/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// 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 static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.util.Set;
-import java.util.zip.ZipOutputStream;
-
-public class ZipFileOutputSink extends FileSystemOutputSink {
-
-  private final ZipOutputStream outputStream;
-
-  public ZipFileOutputSink(Path outputPath, InternalOptions options) throws IOException {
-    super(options);
-    outputStream = new ZipOutputStream(
-        Files.newOutputStream(outputPath, StandardOpenOption.CREATE,
-            StandardOpenOption.TRUNCATE_EXISTING));
-  }
-
-  @Override
-  public void writeDexFile(byte[] contents, Set<String> classDescriptors, int fileId)
-      throws IOException {
-    writeToZipFile(getOutputFileName(fileId), contents);
-  }
-
-  @Override
-  public void writeDexFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
-      throws IOException {
-    writeToZipFile(getOutputFileName(primaryClassName, DEX_EXTENSION), contents);
-  }
-
-  @Override
-  public void close() throws IOException {
-    outputStream.close();
-  }
-
-  private synchronized void writeToZipFile(String outputPath, byte[] content) throws IOException {
-    ZipUtils.writeToZipStream(outputStream, outputPath, content);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
index 13e2438..29ece33 100644
--- a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
@@ -53,9 +53,9 @@
     }
 
     @Override
-    void build(Path testJarFile, Path out) throws Throwable {
+    void build(Path testJarFile, Path out, OutputMode mode) throws Throwable {
       Map<String, Resource> files = compileClassesTogether(testJarFile, null);
-      mergeClassFiles(Lists.newArrayList(files.values()), out);
+      mergeClassFiles(Lists.newArrayList(files.values()), out, mode);
     }
 
     // Dex classes separately.
@@ -180,7 +180,11 @@
     }
 
     Resource mergeClassFiles(List<Resource> dexFiles, Path out) throws Throwable {
-      D8Command.Builder builder = D8Command.builder();
+      return mergeClassFiles(dexFiles, out, OutputMode.Indexed);
+    }
+
+    Resource mergeClassFiles(List<Resource> dexFiles, Path out, OutputMode mode) throws Throwable {
+      D8Command.Builder builder = D8Command.builder().setOutputMode(mode);
       for (Resource dexFile : dexFiles) {
         builder.addDexProgramData(readResource(dexFile), dexFile.getOrigin());
       }
diff --git a/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
index 5351152..77df3ff 100644
--- a/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
@@ -14,7 +14,6 @@
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.OffOrAuto;
 import com.android.tools.r8.utils.OutputMode;
-import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collection;
@@ -44,10 +43,9 @@
       return withBuilderTransformation(b -> b.addClasspathFiles(classpath));
     }
 
-
     @Override
-    void build(Path inputFile, Path out) throws Throwable {
-      D8Command.Builder builder = D8Command.builder();
+    void build(Path inputFile, Path out, OutputMode mode) throws Throwable {
+      D8Command.Builder builder = D8Command.builder().setOutputMode(mode);
       for (UnaryOperator<D8Command.Builder> transformation : builderTransformations) {
         builder = transformation.apply(builder);
       }
@@ -631,11 +629,10 @@
         test(packageName + "intermediate", packageName, "N/A")
             .withInterfaceMethodDesugaring(OffOrAuto.Auto)
             .withMinApiLevel(minApi)
-            .withOptionConsumer(option -> option.outputMode = outputMode)
             .withIntermediate(true);
     Path intermediateDex =
         temp.getRoot().toPath().resolve(packageName + "intermediate" + ZIP_EXTENSION);
-    intermediate.build(input, intermediateDex);
+    intermediate.build(input, intermediateDex, outputMode);
 
     TestRunner<?> end =
         test(packageName + "dex", packageName, "N/A")
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
index a7354e2..936a21f 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.OutputMode;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.nio.file.Path;
@@ -58,8 +59,8 @@
     }
 
     @Override
-    void build(Path inputFile, Path out) throws Throwable {
-      R8Command.Builder builder = R8Command.builder();
+    void build(Path inputFile, Path out, OutputMode mode) throws Throwable {
+      R8Command.Builder builder = R8Command.builder().setOutputMode(mode);
       for (UnaryOperator<R8Command.Builder> transformation : builderTransformations) {
         builder = transformation.apply(builder);
       }
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index 0963631..bb0c129 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -256,7 +256,7 @@
       case R8: {
         ToolHelper.runR8(R8Command.builder()
             .addProgramFiles(getInputFile())
-            .setOutputPath(output == Output.CF ? null : getOutputFile())
+            .setOutputPath(getOutputFile())
             .setMode(mode)
             .build(),
             options -> {
diff --git a/src/test/java/com/android/tools/r8/R8UnreachableCodeTest.java b/src/test/java/com/android/tools/r8/R8UnreachableCodeTest.java
index 7af3b05..97108e1 100644
--- a/src/test/java/com/android/tools/r8/R8UnreachableCodeTest.java
+++ b/src/test/java/com/android/tools/r8/R8UnreachableCodeTest.java
@@ -33,12 +33,11 @@
     AndroidApp input = AndroidApp.fromProgramFiles(SMALI_DIR.resolve(name).resolve(name + ".dex"));
     ExecutorService executorService = Executors.newSingleThreadExecutor();
     Timing timing = new Timing("R8UnreachableCodeTest");
+    InternalOptions options = new InternalOptions();
+    options.programConsumer = DexIndexedConsumer.emptyConsumer();
     DirectMappedDexApplication application =
-        new ApplicationReader(input, new InternalOptions(), timing)
-            .read(executorService)
-            .toDirect();
-    IRConverter converter =
-        new IRConverter(new AppInfoWithSubtyping(application), new InternalOptions());
+        new ApplicationReader(input, options, timing).read(executorService).toDirect();
+    IRConverter converter = new IRConverter(new AppInfoWithSubtyping(application), options);
     converter.optimize(application);
     DexProgramClass clazz = application.classes().iterator().next();
     assertEquals(4, clazz.directMethods().length);
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 3bd3086..2a313a3 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.utils.DexInspector.InvokeInstructionSubject;
 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 com.google.common.collect.ImmutableMap;
 import com.google.common.io.ByteStreams;
@@ -167,7 +168,11 @@
       return self();
     }
 
-    abstract void build(Path inputFile, Path out) throws Throwable;
+    void build(Path inputFile, Path out) throws Throwable {
+      build(inputFile, out, OutputMode.Indexed);
+    }
+
+    abstract void build(Path inputFile, Path out, OutputMode mode) throws Throwable;
   }
 
   private static List<String> minSdkErrorExpected =
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 46a769d..9c88029 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -18,7 +18,7 @@
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.AndroidAppOutputSink;
+import com.android.tools.r8.utils.AndroidAppConsumers;
 import com.android.tools.r8.utils.DefaultDiagnosticsHandler;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
@@ -706,8 +706,8 @@
     if (optionsConsumer != null) {
       optionsConsumer.accept(options);
     }
-    AndroidAppOutputSink compatSink = new AndroidAppOutputSink(command.getOutputSink(), options);
-    R8.runForTesting(app, compatSink, options);
+    AndroidAppConsumers compatSink = new AndroidAppConsumers(options);
+    R8.runForTesting(app, options);
     return compatSink.build();
   }
 
@@ -729,8 +729,8 @@
       throw new RuntimeException(e);
     }
     InternalOptions options = command.getInternalOptions();
-    AndroidAppOutputSink compatSink = new AndroidAppOutputSink(command.getOutputSink(), options);
-    R8.runForTesting(command.getInputApp(), compatSink, options);
+    AndroidAppConsumers compatSink = new AndroidAppConsumers(options);
+    R8.runForTesting(command.getInputApp(), options);
     return compatSink.build();
   }
 
@@ -757,8 +757,8 @@
     if (optionsConsumer != null) {
       optionsConsumer.accept(options);
     }
-    AndroidAppOutputSink compatSink = new AndroidAppOutputSink(command.getOutputSink(), options);
-    D8.runForTesting(command.getInputApp(), compatSink, options);
+    AndroidAppConsumers compatSink = new AndroidAppConsumers(options);
+    D8.runForTesting(command.getInputApp(), options);
     return compatSink.build();
   }
 
diff --git a/src/test/java/com/android/tools/r8/dex/SharedClassWritingTest.java b/src/test/java/com/android/tools/r8/dex/SharedClassWritingTest.java
index 1f5df06..b99bf6d 100644
--- a/src/test/java/com/android/tools/r8/dex/SharedClassWritingTest.java
+++ b/src/test/java/com/android/tools/r8/dex/SharedClassWritingTest.java
@@ -3,11 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.dex;
 
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.code.ConstString;
 import com.android.tools.r8.code.Instruction;
 import com.android.tools.r8.code.ReturnVoid;
 import com.android.tools.r8.errors.DexOverflowException;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexAnnotationSetRefList;
@@ -28,9 +29,7 @@
 import com.android.tools.r8.origin.SynthesizedOrigin;
 import com.android.tools.r8.utils.DefaultDiagnosticsHandler;
 import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.IgnoreContentsOutputSink;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -131,16 +130,16 @@
     classes.forEach(builder::addProgramClass);
     DexApplication application = builder.build();
 
+    CollectInfoConsumer consumer = new CollectInfoConsumer();
     InternalOptions options = new InternalOptions(dexItemFactory,
         new Reporter(new DefaultDiagnosticsHandler()));
-    options.outputMode = OutputMode.FilePerInputClass;
+    options.programConsumer = consumer;
     ApplicationWriter writer =
         new ApplicationWriter(
             application, options, null, null, NamingLens.getIdentityLens(), null, null);
     ExecutorService executorService = ThreadUtils.getExecutorService(options);
-    CollectInfoOutputSink sink = new CollectInfoOutputSink();
-    writer.write(sink, executorService);
-    List<Set<String>> generatedDescriptors = sink.getDescriptors();
+    writer.write(executorService);
+    List<Set<String>> generatedDescriptors = consumer.getDescriptors();
     // Check all files present.
     Assert.assertEquals(NUMBER_OF_FILES, generatedDescriptors.size());
     // And each file contains two classes of which one is the shared one.
@@ -151,23 +150,28 @@
     }
   }
 
-  private static class CollectInfoOutputSink extends IgnoreContentsOutputSink {
+  private static class CollectInfoConsumer implements DexFilePerClassFileConsumer {
 
     private final List<Set<String>> descriptors = new ArrayList<>();
 
     @Override
-    public void writeDexFile(byte[] contents, Set<String> classDescriptors, int fileId) {
-      throw new Unreachable();
+    public void accept(
+        String primaryClassDescriptor,
+        byte[] data,
+        Set<String> descriptors,
+        DiagnosticsHandler handler) {
+      addDescriptors(descriptors);
     }
 
-    @Override
-    public synchronized void writeDexFile(byte[] contents, Set<String> classDescriptors,
-        String primaryClassName) {
-      descriptors.add(classDescriptors);
+    synchronized void addDescriptors(Set<String> descriptors) {
+      this.descriptors.add(descriptors);
     }
 
     public List<Set<String>> getDescriptors() {
       return descriptors;
     }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {}
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java b/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
index 4ac8f0e..2656467 100644
--- a/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
+++ b/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
@@ -20,7 +20,7 @@
 import com.android.tools.r8.smali.SmaliBuilder.MethodSignature;
 import com.android.tools.r8.smali.SmaliTestBase;
 import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.AndroidAppOutputSink;
+import com.android.tools.r8.utils.AndroidAppConsumers;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Timing;
@@ -73,6 +73,7 @@
     public final List<IRCode> additionalCode;
     public final ValueNumberGenerator valueNumberGenerator;
     public final InternalOptions options;
+    public final AndroidAppConsumers consumers;
 
     public TestApplication(
         DexApplication application,
@@ -96,6 +97,7 @@
       this.additionalCode = additionalCode;
       this.valueNumberGenerator = valueNumberGenerator;
       this.options = options;
+      consumers = new AndroidAppConsumers(options);
     }
 
     public int countArgumentInstructions() {
@@ -118,20 +120,17 @@
     private AndroidApp writeDex(DexApplication application, InternalOptions options)
         throws DexOverflowException {
       try {
-        AndroidAppOutputSink compatSink = new AndroidAppOutputSink();
         R8.writeApplication(
             Executors.newSingleThreadExecutor(),
             application,
-            compatSink,
             null,
             NamingLens.getIdentityLens(),
             null,
             options,
             null);
-        options.closeProgramConsumer();
-        compatSink.close();
-        return compatSink.build();
-      } catch (ExecutionException | IOException e) {
+        options.signalFinishedToProgramConsumer();
+        return consumers.build();
+      } catch (ExecutionException e) {
         throw new RuntimeException(e);
       }
     }
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 9d9702f..b86a2cd 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -51,7 +51,7 @@
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.AndroidAppOutputSink;
+import com.android.tools.r8.utils.AndroidAppConsumers;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
@@ -624,14 +624,13 @@
         new ApplicationWriter(
             application, options, null, null, NamingLens.getIdentityLens(), null, null);
     ExecutorService executor = ThreadUtils.getExecutorService(options);
-    AndroidAppOutputSink compatSink = new AndroidAppOutputSink();
+    AndroidAppConsumers compatSink = new AndroidAppConsumers(options);
     try {
-      writer.write(compatSink, executor);
+      writer.write(executor);
     } finally {
       executor.shutdown();
     }
-    compatSink.close();
-    options.closeProgramConsumer();
+    options.signalFinishedToProgramConsumer();
     return compatSink.build();
   }
 
diff --git a/src/test/java/com/android/tools/r8/utils/Smali.java b/src/test/java/com/android/tools/r8/utils/Smali.java
index 64bc82d..7d95e7f 100644
--- a/src/test/java/com/android/tools/r8/utils/Smali.java
+++ b/src/test/java/com/android/tools/r8/utils/Smali.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.dex.ApplicationWriter;
 import com.android.tools.r8.errors.DexOverflowException;
@@ -98,8 +100,10 @@
     // This returns the full backingstore from MemoryDataStore, which by default is 1024k bytes.
     // We process it via our reader and writer to trim it to the exact size and update its checksum.
     byte[] data = dataStore.getData();
+    SingleFileConsumer consumer = new SingleFileConsumer();
     AndroidApp app = AndroidApp.fromDexProgramData(data);
     InternalOptions options = new InternalOptions();
+    options.programConsumer = consumer;
     ExecutorService executor = ThreadUtils.getExecutorService(1);
     try {
       DexApplication dexApp = new ApplicationReader(
@@ -107,29 +111,24 @@
       ApplicationWriter writer =
           new ApplicationWriter(
               dexApp, options, null, null, NamingLens.getIdentityLens(), null, null);
-      SingleFileSink sink = new SingleFileSink();
-      writer.write(sink, executor);
-      return sink.contents;
+      writer.write(executor);
+      return consumer.contents;
     } finally {
       executor.shutdown();
     }
   }
 
-  private static class SingleFileSink extends IgnoreContentsOutputSink {
+  private static class SingleFileConsumer implements DexIndexedConsumer {
 
     byte[] contents;
 
     @Override
-    public void writeDexFile(byte[] contents, Set<String> classDescriptors,
-        String primaryClassName) {
-      assert contents != null;
-      this.contents = contents;
+    public void accept(
+        int fileIndex, byte[] data, Set<String> descriptors, DiagnosticsHandler handler) {
+      contents = data;
     }
 
     @Override
-    public void writeDexFile(byte[] contents, Set<String> classDescriptors, int fileId) {
-      assert contents != null;
-      this.contents = contents;
-    }
+    public void finished(DiagnosticsHandler handler) {}
   }
 }