Introduce program consumers in the public API and deprecate setOutputPath and setOutputMode.

The configuration of output can now be done in one of two ways:

1. Set output path and mode using setOutputPath and setOutputMode on the builder.

2. Set a program consumer by using setProgramConsumer or setOutput(Path, OutputMode). In
   both cases a program consumer is defined and will be used for receiveing output.

For R8 only API 2 is supported. The R8 command-line has been updated to allow the
mutually exclusive flags --dex and --classfile denoting the output format.
The implicit default is --dex.

For D8 both 1 and 2 are supported now, but they are mutually exclusive. The builder will
abort if a mix of the two appear.

For D8 we retain a backwards-compatible default set-up using the deprecated API, such that a
default builder, eg, D8Command.builder().build() will an empty dex-indexed consumer. This
behaviour is deprecated and for R8 will result in an error.

Change-Id: I3b387d4bcd6b0921e7e6f46b22f6d8244c7132b0
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
index 4a2f7b0..eb880f2 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DefaultDiagnosticsHandler;
@@ -18,8 +20,21 @@
  */
 abstract class BaseCompilerCommand extends BaseCommand {
 
-  private final ProgramConsumer programConsumer;
+  // TODO(b/70656566): Remove this once the deprecated API is removed.
+  protected static class OutputOptions {
+    final Path path;
+    final OutputMode mode;
+
+    public OutputOptions(Path path, OutputMode mode) {
+      this.path = path;
+      this.mode = mode;
+    }
+  }
+
+  private final OutputOptions outputOptions;
+
   private final CompilationMode mode;
+  private final ProgramConsumer programConsumer;
   private final int minApiLevel;
   private final Reporter reporter;
   private final boolean enableDesugaring;
@@ -31,12 +46,14 @@
     minApiLevel = 0;
     reporter = new Reporter(new DefaultDiagnosticsHandler());
     enableDesugaring = true;
+    outputOptions = null;
   }
 
   BaseCompilerCommand(
       AndroidApp app,
       CompilationMode mode,
       ProgramConsumer programConsumer,
+      OutputOptions outputOptions,
       int minApiLevel,
       Reporter reporter,
       boolean enableDesugaring) {
@@ -48,11 +65,7 @@
     this.minApiLevel = minApiLevel;
     this.reporter = reporter;
     this.enableDesugaring = enableDesugaring;
-  }
-
-  @Deprecated
-  public Path getOutputPath() {
-    return programConsumer == null ? null : programConsumer.getOutputPath();
+    this.outputOptions = outputOptions;
   }
 
   public CompilationMode getMode() {
@@ -64,10 +77,19 @@
   }
 
   @Deprecated
+  public Path getOutputPath() {
+    if (outputOptions == null) {
+      throw new CompilationError("Use of deprecated API may not be used with new consumer API");
+    }
+    return outputOptions.path;
+  }
+
+  @Deprecated
   public OutputMode getOutputMode() {
-    return programConsumer instanceof DexFilePerClassFileConsumer
-        ? OutputMode.FilePerInputClass
-        : OutputMode.Indexed;
+    if (outputOptions == null) {
+      throw new CompilationError("Use of deprecated API may not be used with new consumer API");
+    }
+    return outputOptions.mode;
   }
 
   public ProgramConsumer getProgramConsumer() {
@@ -85,8 +107,11 @@
   abstract public static class Builder<C extends BaseCompilerCommand, B extends Builder<C, B>>
       extends BaseCommand.Builder<C, B> {
 
+    private ProgramConsumer programConsumer = null;
     private Path outputPath = null;
+    // TODO(b/70656566): Remove default output mode when deprecated API is removed.
     private OutputMode outputMode = OutputMode.Indexed;
+
     private CompilationMode mode;
     private int minApiLevel = AndroidApiLevel.getDefault().getLevel();
     private boolean enableDesugaring = true;
@@ -124,7 +149,10 @@
     }
 
     /**
-     * Get the output path. Null if not set.
+     * Get the output path.
+     *
+     * @return Current output path, null if no output path-and-mode have been set.
+     * @see #setOutput(Path, OutputMode)
      */
     public Path getOutputPath() {
       return outputPath;
@@ -132,29 +160,109 @@
 
     /**
      * Get the output mode.
+     *
+     * @return Currently set output mode, null if no output path-and-mode have been set.
+     * @see #setOutput(Path, OutputMode)
      */
     public OutputMode getOutputMode() {
       return outputMode;
     }
 
-    /** Set an output path. Must be an existing directory or a zip file. */
+    /**
+     * Get the program consumer.
+     *
+     * @return The currently set program consumer, null if no program consumer or output
+     *     path-and-mode is set, e.g., neither {@link #setProgramConsumer} nor
+     *     {@link #setOutput} have been called.
+     */
+    public ProgramConsumer getProgramConsumer() {
+      return programConsumer;
+    }
+
+    /**
+     * Set the program consumer.
+     *
+     * <p>Setting the program consumer will override any previous set consumer or any previous set
+     * output path & mode.
+     *
+     * @param programConsumer Program consumer to set as current. A null argument will clear the
+     *     program consumer / output.
+     */
+    public B setProgramConsumer(ProgramConsumer programConsumer) {
+      // Setting an explicit program consumer resets any output-path/mode setup.
+      outputPath = null;
+      outputMode = null;
+      this.programConsumer = programConsumer;
+      return self();
+    }
+
+    /**
+     * Set the output path-and-mode.
+     *
+     * <p>Setting the output path-and-mode will override any previous set consumer or any previous
+     * output path-and-mode, and implicitly sets the appropriate program consumer to write the
+     * output.
+     *
+     * @param outputPath Path to write the output to. Must be an archive or and existing directory.
+     * @param outputMode Mode in which to write the output.
+     */
+    public B setOutput(Path outputPath, OutputMode outputMode) {
+      assert outputPath != null;
+      assert outputMode != null;
+      assert !outputMode.isDeprecated();
+      this.outputPath = outputPath;
+      this.outputMode = outputMode;
+      programConsumer = createProgramOutputConsumer(outputPath, outputMode);
+      return self();
+    }
+
+    /**
+     * Set an output path. Must be an existing directory or a zip file.
+     *
+     * @see #setOutput
+     */
     @Deprecated
     public B setOutputPath(Path outputPath) {
+      // Ensure this is not mixed with uses of the new consumer API.
+      assert programConsumer == null;
       this.outputPath = outputPath;
       return self();
     }
 
-    /** Set an output mode. */
+    /**
+     * Set an output mode.
+     *
+     * @see #setOutput
+     */
     @Deprecated
     public B setOutputMode(OutputMode outputMode) {
+      // Ensure this is not mixed with uses of the new consumer API.
+      assert programConsumer == null;
+      assert outputMode == null || outputMode.isDeprecated();
+      assert this.outputMode == null || this.outputMode.isDeprecated();
       this.outputMode = outputMode;
       return self();
     }
 
-    public ProgramConsumer getProgramConsumer() {
-      return getOutputMode() == OutputMode.Indexed
-          ? createIndexedConsumer()
-          : createPerClassFileConsumer();
+    private InternalProgramOutputPathConsumer createProgramOutputConsumer(
+        Path path,
+        OutputMode mode) {
+      if (mode.isDexIndexed()) {
+        return FileUtils.isArchive(path)
+            ? new DexIndexedConsumer.ArchiveConsumer(path)
+            : new DexIndexedConsumer.DirectoryConsumer(path);
+      }
+      if (mode.isDexFilePerClassFile()) {
+        return FileUtils.isArchive(path)
+            ? new DexFilePerClassFileConsumer.ArchiveConsumer(path)
+            : new DexFilePerClassFileConsumer.DirectoryConsumer(path);
+      }
+      if (mode.isClassFile()) {
+        return FileUtils.isArchive(path)
+            ? new ClassFileConsumer.ArchiveConsumer(path)
+            : new ClassFileConsumer.DirectoryConsumer(path);
+      }
+      throw new Unreachable("Unexpected output mode: " + mode);
     }
 
     /**
@@ -185,25 +293,8 @@
     @Override
     protected void validate() {
       assert mode != null;
-      if (getAppBuilder().hasMainDexList() && outputMode == OutputMode.FilePerInputClass) {
-        reporter.error("Option --main-dex-list cannot be used with --file-per-class");
-      }
       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);
-    }
   }
 }