Revert "Remove CompatDx"

This reverts commit 2a674cef09b6c60ce0c79e94967d46280550e8a2.

CompatDx is still in use.

Added a keep rule for the CompatDx class as well.

Change-Id: I87278684a159d7207c61d1f3bd9158ed2e65570e
diff --git a/src/main/java/com/android/tools/r8/CompatDexHelper.java b/src/main/java/com/android/tools/r8/CompatDexHelper.java
deleted file mode 100644
index fa52e25..0000000
--- a/src/main/java/com/android/tools/r8/CompatDexHelper.java
+++ /dev/null
@@ -1,11 +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;
-
-public class CompatDexHelper {
-  public static void ignoreDexInArchive(BaseCommand.Builder builder) {
-    builder.setIgnoreDexInArchive(true);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/CompatDxHelper.java b/src/main/java/com/android/tools/r8/CompatDxHelper.java
new file mode 100644
index 0000000..a84bab5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/CompatDxHelper.java
@@ -0,0 +1,30 @@
+// 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.InternalOptions;
+
+public class CompatDxHelper {
+  public static void run(D8Command command, Boolean minimalMainDex)
+      throws CompilationFailedException {
+    AndroidApp app = command.getInputApp();
+    InternalOptions options = command.getInternalOptions();
+    // DX does not desugar.
+    options.enableDesugaring = false;
+    // DX allows --multi-dex without specifying a main dex list for legacy devices.
+    // That is broken, but for CompatDX we do the same to not break existing builds
+    // that are trying to transition.
+    options.enableMainDexListCheck = false;
+    // DX has a minimal main dex flag. In compat mode only do minimal main dex
+    // if the flag is actually set.
+    options.minimalMainDex = minimalMainDex;
+    D8.runForTesting(app, options);
+  }
+
+  public static void ignoreDexInArchive(BaseCommand.Builder builder) {
+    builder.setIgnoreDexInArchive(true);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/D8Logger.java b/src/main/java/com/android/tools/r8/D8Logger.java
index a3015dc..6c3fca0 100644
--- a/src/main/java/com/android/tools/r8/D8Logger.java
+++ b/src/main/java/com/android/tools/r8/D8Logger.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import com.android.tools.r8.compatdx.CompatDx;
 import com.google.common.collect.ImmutableList;
 import java.io.FileWriter;
 import java.io.IOException;
@@ -17,7 +18,8 @@
       "Usage: java -jar d8logger.jar <compiler-options>",
       " where <compiler-options> will be",
       "",
-      " 1. forwarded to the 'd8' tool, and also",
+      " 1. forwarded to the 'd8' or 'compatdx' tool (depending on the presence of the '--dex'",
+      "    option), and also",
       " 2. appended to the file in the environment variable 'D8LOGGER_OUTPUT'",
       "",
       " The options will be appended as a new line with TAB characters between the arguments."));
@@ -41,6 +43,10 @@
       }
     }
 
-    D8.main(args);
+    if (Arrays.stream(args).anyMatch(s -> s.equals("--dex"))) {
+      CompatDx.main(args);
+    } else {
+      D8.main(args);
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/SwissArmyKnife.java b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
index e352ede..f54233b 100644
--- a/src/main/java/com/android/tools/r8/SwissArmyKnife.java
+++ b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.bisect.Bisect;
+import com.android.tools.r8.compatdx.CompatDx;
 import com.android.tools.r8.compatproguard.CompatProguard;
 import com.android.tools.r8.dexfilemerger.DexFileMerger;
 import com.android.tools.r8.dexsplitter.DexSplitter;
@@ -30,6 +31,9 @@
       case "bisect":
         Bisect.main(shift(args));
         break;
+      case "compatdx":
+        CompatDx.main(shift(args));
+        break;
       case "compatproguard":
         CompatProguard.main(shift(args));
         break;
diff --git a/src/main/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilder.java b/src/main/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilder.java
index f7e6dc3..f4f3c7e 100644
--- a/src/main/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilder.java
+++ b/src/main/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilder.java
@@ -4,7 +4,7 @@
 package com.android.tools.r8.compatdexbuilder;
 
 import com.android.tools.r8.ByteDataView;
-import com.android.tools.r8.CompatDexHelper;
+import com.android.tools.r8.CompatDxHelper;
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.D8;
@@ -162,7 +162,7 @@
       throws IOException, CompilationFailedException {
     DexConsumer consumer = new DexConsumer();
     D8Command.Builder builder = D8Command.builder();
-    CompatDexHelper.ignoreDexInArchive(builder);
+    CompatDxHelper.ignoreDexInArchive(builder);
     builder
         .setProgramConsumer(consumer)
         .setMode(noLocals ? CompilationMode.RELEASE : CompilationMode.DEBUG)
diff --git a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
new file mode 100644
index 0000000..a52c2ea
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
@@ -0,0 +1,618 @@
+// 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.compatdx;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.CompatDxHelper;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.Version;
+import com.android.tools.r8.compatdx.CompatDx.DxCompatOptions.DxUsageMessage;
+import com.android.tools.r8.compatdx.CompatDx.DxCompatOptions.PositionInfo;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import joptsimple.OptionParser;
+import joptsimple.OptionSet;
+import joptsimple.OptionSpec;
+
+/**
+ * Dx compatibility interface for d8.
+ *
+ * This should become a mostly drop-in replacement for uses of the DX dexer (eg, dx --dex ...).
+ */
+public class CompatDx {
+
+  private static final String USAGE_HEADER = "Usage: compatdx [options] <input files>";
+
+  /**
+   * Compatibility options parsing for the DX --dex sub-command.
+   */
+  public static class DxCompatOptions {
+    // Final values after parsing.
+    // Note: These are ordered by their occurrence in "dx --help"
+    public final boolean help;
+    public final boolean version;
+    public final boolean debug;
+    public final boolean verbose;
+    public final PositionInfo positions;
+    public final boolean noLocals;
+    public final boolean noOptimize;
+    public final boolean statistics;
+    public final String optimizeList;
+    public final String noOptimizeList;
+    public final boolean noStrict;
+    public final boolean keepClasses;
+    public final String output;
+    public final String dumpTo;
+    public final int dumpWidth;
+    public final String dumpMethod;
+    public final boolean verboseDump;
+    public final boolean dump;
+    public final boolean noFiles;
+    public final boolean coreLibrary;
+    public final int numThreads;
+    public final boolean incremental;
+    public final boolean forceJumbo;
+    public final boolean noWarning;
+    public final boolean multiDex;
+    public final String mainDexList;
+    public final boolean minimalMainDex;
+    public final int minApiLevel;
+    public final String inputList;
+    public final ImmutableList<String> inputs;
+    // Undocumented option
+    public final int maxIndexNumber;
+
+    private static final String FILE_ARG = "file";
+    private static final String NUM_ARG = "number";
+    private static final String METHOD_ARG = "method";
+
+    public enum PositionInfo {
+      NONE, IMPORTANT, LINES, THROWING
+    }
+
+    // Exception thrown on invalid dx compat usage.
+    public static class DxUsageMessage extends Exception {
+      public final String message;
+
+      DxUsageMessage(String message) {
+        this.message = message;
+      }
+
+      void printHelpOn(PrintStream sink) throws IOException {
+        sink.println(message);
+      }
+    }
+
+    // Parsing specification.
+    private static class Spec {
+      final OptionParser parser;
+
+      // Note: These are ordered by their occurrence in "dx --help"
+      final OptionSpec<Void> debug;
+      final OptionSpec<Void> verbose;
+      final OptionSpec<String> positions;
+      final OptionSpec<Void> noLocals;
+      final OptionSpec<Void> noOptimize;
+      final OptionSpec<Void> statistics;
+      final OptionSpec<String> optimizeList;
+      final OptionSpec<String> noOptimizeList;
+      final OptionSpec<Void> noStrict;
+      final OptionSpec<Void> keepClasses;
+      final OptionSpec<String> output;
+      final OptionSpec<String> dumpTo;
+      final OptionSpec<Integer> dumpWidth;
+      final OptionSpec<String> dumpMethod;
+      final OptionSpec<Void> dump;
+      final OptionSpec<Void> verboseDump;
+      final OptionSpec<Void> noFiles;
+      final OptionSpec<Void> coreLibrary;
+      final OptionSpec<Integer> numThreads;
+      final OptionSpec<Void> incremental;
+      final OptionSpec<Void> forceJumbo;
+      final OptionSpec<Void> noWarning;
+      final OptionSpec<Void> multiDex;
+      final OptionSpec<String> mainDexList;
+      final OptionSpec<Void> minimalMainDex;
+      final OptionSpec<Integer> minApiLevel;
+      final OptionSpec<String> inputList;
+      final OptionSpec<String> inputs;
+      final OptionSpec<Void> version;
+      final OptionSpec<Void> help;
+      final OptionSpec<Integer> maxIndexNumber;
+
+      Spec() {
+        parser = new OptionParser();
+        parser.accepts("dex");
+        debug = parser.accepts("debug", "Print debug information");
+        verbose = parser.accepts("verbose", "Print verbose information");
+        positions = parser
+            .accepts("positions",
+                "What source-position information to keep. One of: none, lines, important")
+            .withOptionalArg()
+            .describedAs("keep")
+            .defaultsTo("lines");
+        noLocals = parser.accepts("no-locals", "Don't keep local variable information");
+        statistics = parser.accepts("statistics", "Print statistics information");
+        noOptimize = parser.accepts("no-optimize", "Don't optimize");
+        optimizeList = parser
+            .accepts("optimize-list", "File listing methods to optimize")
+            .withRequiredArg()
+            .describedAs(FILE_ARG);
+        noOptimizeList = parser
+            .accepts("no-optimize-list", "File listing methods not to optimize")
+            .withRequiredArg()
+            .describedAs(FILE_ARG);
+        noStrict = parser.accepts("no-strict", "Disable strict file/class name checks");
+        keepClasses = parser.accepts("keep-classes", "Keep input class files in in output jar");
+        output = parser
+            .accepts("output", "Output file or directory")
+            .withRequiredArg()
+            .describedAs(FILE_ARG);
+        dumpTo = parser
+            .accepts("dump-to", "File to dump information to")
+            .withRequiredArg()
+            .describedAs(FILE_ARG);
+        dumpWidth = parser
+            .accepts("dump-width", "Max width for columns in dump output")
+            .withRequiredArg()
+            .ofType(Integer.class)
+            .defaultsTo(0)
+            .describedAs(NUM_ARG);
+        dumpMethod = parser
+            .accepts("dump-method", "Method to dump information for")
+            .withRequiredArg()
+            .describedAs(METHOD_ARG);
+        dump = parser.accepts("dump", "Dump information");
+        verboseDump = parser.accepts("verbose-dump", "Dump verbose information");
+        noFiles = parser.accepts("no-files", "Don't fail if given no files");
+        coreLibrary = parser.accepts("core-library", "Construct a core library");
+        numThreads = parser
+            .accepts("num-threads", "Number of threads to run with")
+            .withRequiredArg()
+            .ofType(Integer.class)
+            .defaultsTo(1)
+            .describedAs(NUM_ARG);
+        incremental = parser.accepts("incremental", "Merge result with the output if it exists");
+        forceJumbo = parser.accepts("force-jumbo", "Force use of string-jumbo instructions");
+        noWarning = parser.accepts("no-warning", "Suppress warnings");
+        maxIndexNumber = parser.accepts("set-max-idx-number",
+            "Undocumented: Set maximal index number to use in a dex file.")
+            .withRequiredArg()
+            .ofType(Integer.class)
+            .defaultsTo(0)
+            .describedAs("Maximum index");
+        minimalMainDex = parser.accepts("minimal-main-dex", "Produce smallest possible main dex");
+        mainDexList = parser
+            .accepts("main-dex-list", "File listing classes that must be in the main dex file")
+            .withRequiredArg()
+            .describedAs(FILE_ARG);
+        multiDex =
+            parser
+                .accepts("multi-dex", "Allow generation of multi-dex")
+                .requiredIf(minimalMainDex, mainDexList, maxIndexNumber);
+        minApiLevel = parser
+            .accepts("min-sdk-version", "Minimum Android API level compatibility.")
+            .withRequiredArg().ofType(Integer.class);
+        inputList = parser
+            .accepts("input-list", "File listing input files")
+            .withRequiredArg()
+            .describedAs(FILE_ARG);
+        inputs = parser.nonOptions("Input files");
+        version = parser.accepts("version", "Print the version of this tool").forHelp();
+        help = parser.accepts("help", "Print this message").forHelp();
+      }
+    }
+
+    private DxCompatOptions(OptionSet options, Spec spec) {
+      help = options.has(spec.help);
+      version = options.has(spec.version);
+      debug = options.has(spec.debug);
+      verbose = options.has(spec.verbose);
+      if (options.has(spec.positions)) {
+        switch (options.valueOf(spec.positions)) {
+          case "none":
+            positions = PositionInfo.NONE;
+            break;
+          case "important":
+            positions = PositionInfo.IMPORTANT;
+            break;
+          case "lines":
+            positions = PositionInfo.LINES;
+            break;
+          case "throwing":
+            positions = PositionInfo.THROWING;
+            break;
+          default:
+            positions = PositionInfo.IMPORTANT;
+            break;
+        }
+      } else {
+        positions = PositionInfo.LINES;
+      }
+      noLocals = options.has(spec.noLocals);
+      noOptimize = options.has(spec.noOptimize);
+      statistics = options.has(spec.statistics);
+      optimizeList = options.valueOf(spec.optimizeList);
+      noOptimizeList = options.valueOf(spec.noOptimizeList);
+      noStrict = options.has(spec.noStrict);
+      keepClasses = options.has(spec.keepClasses);
+      output = options.valueOf(spec.output);
+      dumpTo = options.valueOf(spec.dumpTo);
+      dumpWidth = options.valueOf(spec.dumpWidth);
+      dumpMethod = options.valueOf(spec.dumpMethod);
+      dump = options.has(spec.dump);
+      verboseDump = options.has(spec.verboseDump);
+      noFiles = options.has(spec.noFiles);
+      coreLibrary = options.has(spec.coreLibrary);
+      numThreads = lastIntOf(options.valuesOf(spec.numThreads));
+      incremental = options.has(spec.incremental);
+      forceJumbo = options.has(spec.forceJumbo);
+      noWarning = options.has(spec.noWarning);
+      multiDex = options.has(spec.multiDex);
+      mainDexList = options.valueOf(spec.mainDexList);
+      minimalMainDex = options.has(spec.minimalMainDex);
+      if (options.has(spec.minApiLevel)) {
+        List<Integer> allMinApiLevels = options.valuesOf(spec.minApiLevel);
+        minApiLevel = allMinApiLevels.get(allMinApiLevels.size() - 1);
+      } else {
+        minApiLevel = AndroidApiLevel.getDefault().getLevel();
+      }
+      inputList = options.valueOf(spec.inputList);
+      inputs = ImmutableList.copyOf(options.valuesOf(spec.inputs));
+      maxIndexNumber = options.valueOf(spec.maxIndexNumber);
+    }
+
+    public static DxCompatOptions parse(String[] args) {
+      Spec spec = new Spec();
+      return new DxCompatOptions(spec.parser.parse(args), spec);
+    }
+
+    private static int lastIntOf(List<Integer> values) {
+      assert !values.isEmpty();
+      return values.get(values.size() - 1);
+    }
+  }
+
+  public static void main(String[] args) throws IOException {
+    try {
+      run(args);
+    } catch (DxUsageMessage e) {
+      System.err.println(USAGE_HEADER);
+      e.printHelpOn(System.err);
+      System.exit(1);
+    } catch (CompilationFailedException e) {
+      System.exit(1);
+    }
+  }
+
+  private static void run(String[] args)
+      throws DxUsageMessage, IOException, CompilationFailedException {
+    DxCompatOptions dexArgs = DxCompatOptions.parse(args);
+    if (dexArgs.help) {
+      printHelpOn(System.out);
+      return;
+    }
+    if (dexArgs.version) {
+      System.out.println("CompatDx " + Version.getVersionString());
+      return;
+    }
+    CompilationMode mode = CompilationMode.RELEASE;
+    Path output = null;
+    List<Path> inputs = new ArrayList<>();
+    boolean singleDexFile = !dexArgs.multiDex;
+    Path mainDexList = null;
+    int numberOfThreads = 1;
+
+    for (String path : dexArgs.inputs) {
+      processPath(new File(path), inputs);
+    }
+    if (inputs.isEmpty()) {
+      if (dexArgs.noFiles) {
+        return;
+      }
+      throw new DxUsageMessage("No input files specified");
+    }
+
+    if (!Log.ENABLED && dexArgs.debug) {
+      System.out.println("Warning: logging is not enabled for this build.");
+    }
+
+    if (dexArgs.dump && dexArgs.verbose) {
+      System.out.println("Warning: dump is not supported");
+    }
+
+    if (dexArgs.verboseDump) {
+      throw new Unimplemented("verbose dump file not yet supported");
+    }
+
+    if (dexArgs.dumpMethod != null) {
+      throw new Unimplemented("method-dump not yet supported");
+    }
+
+    if (dexArgs.output != null) {
+      output = Paths.get(dexArgs.output);
+      if (FileUtils.isDexFile(output)) {
+        if (!singleDexFile) {
+          throw new DxUsageMessage("Cannot output to a single dex-file when running with multidex");
+        }
+      } else if (!FileUtils.isArchive(output)
+          && (!output.toFile().exists() || !output.toFile().isDirectory())) {
+        throw new DxUsageMessage("Unsupported output file or output directory does not exist. "
+            + "Output must be a directory or a file of type dex, apk, jar or zip.");
+      }
+    }
+
+    if (dexArgs.dumpTo != null && dexArgs.verbose) {
+      System.out.println("dump-to file not yet supported");
+    }
+
+    if (dexArgs.positions == PositionInfo.NONE && dexArgs.verbose) {
+      System.out.println("Warning: no support for positions none.");
+    }
+
+    if (dexArgs.positions == PositionInfo.LINES && !dexArgs.noLocals) {
+      mode = CompilationMode.DEBUG;
+    }
+
+    if (dexArgs.incremental) {
+      throw new Unimplemented("incremental merge not supported yet");
+    }
+
+    if (dexArgs.forceJumbo && dexArgs.verbose) {
+      System.out.println(
+          "Warning: no support for forcing jumbo-strings.\n"
+              + "Strings will only use jumbo-string indexing if necessary.\n"
+              + "Make sure that any dex merger subsequently used "
+              + "supports correct handling of jumbo-strings (eg, D8/R8 does).");
+    }
+
+    if (dexArgs.noOptimize && dexArgs.verbose) {
+      System.out.println("Warning: no support for not optimizing");
+    }
+
+    if (dexArgs.optimizeList != null) {
+      throw new Unimplemented("no support for optimize-method list");
+    }
+
+    if (dexArgs.noOptimizeList != null) {
+      throw new Unimplemented("no support for dont-optimize-method list");
+    }
+
+    if (dexArgs.statistics && dexArgs.verbose) {
+      System.out.println("Warning: no support for printing statistics");
+    }
+
+    if (dexArgs.numThreads > 1) {
+      numberOfThreads = dexArgs.numThreads;
+    }
+
+    if (dexArgs.mainDexList != null) {
+      mainDexList = Paths.get(dexArgs.mainDexList);
+    }
+
+    if (dexArgs.noStrict) {
+      if (dexArgs.verbose) {
+        System.out.println("Warning: conservative main-dex list not yet supported");
+      }
+    } else {
+      if (dexArgs.verbose) {
+        System.out.println("Warning: strict name checking not yet supported");
+      }
+    }
+
+    if (dexArgs.minimalMainDex && dexArgs.verbose) {
+      System.out.println("Warning: minimal main-dex support is not yet supported");
+    }
+
+    if (dexArgs.maxIndexNumber != 0 && dexArgs.verbose) {
+      System.out.println("Warning: internal maximum-index setting is not supported");
+    }
+
+    if (numberOfThreads < 1) {
+      throw new DxUsageMessage("Invalid numThreads value of " + numberOfThreads);
+    }
+    ExecutorService executor = ThreadUtils.getExecutorService(numberOfThreads);
+
+    try {
+      D8Command.Builder builder = D8Command.builder();
+      CompatDxHelper.ignoreDexInArchive(builder);
+      builder
+          .addProgramFiles(inputs)
+          .setProgramConsumer(
+              createConsumer(inputs, output, singleDexFile, dexArgs.keepClasses))
+          .setMode(mode)
+          .setMinApiLevel(dexArgs.minApiLevel);
+      if (mainDexList != null) {
+        builder.addMainDexListFiles(mainDexList);
+      }
+      CompatDxHelper.run(builder.build(), dexArgs.minimalMainDex);
+    } finally {
+      executor.shutdown();
+    }
+  }
+
+  private static ProgramConsumer createConsumer(
+      List<Path> inputs, Path output, boolean singleDexFile, boolean keepClasses)
+      throws DxUsageMessage {
+    if (output == null) {
+      return DexIndexedConsumer.emptyConsumer();
+    }
+    if (singleDexFile) {
+      return new SingleDexFileConsumer(
+          FileUtils.isDexFile(output)
+              ? new NamedDexFileConsumer(output)
+              : createDexConsumer(output, inputs, keepClasses));
+    }
+    return createDexConsumer(output, inputs, keepClasses);
+  }
+
+  private static DexIndexedConsumer createDexConsumer(
+      Path output, List<Path> inputs, boolean keepClasses)
+      throws DxUsageMessage {
+    if (keepClasses) {
+      if (!FileUtils.isArchive(output)) {
+        throw new DxCompatOptions.DxUsageMessage(
+            "Output must be an archive when --keep-classes is set.");
+      }
+      return new DexKeepClassesConsumer(output, inputs);
+    }
+    return FileUtils.isArchive(output)
+        ? new DexIndexedConsumer.ArchiveConsumer(output)
+        : new DexIndexedConsumer.DirectoryConsumer(output);
+  }
+
+  private static class SingleDexFileConsumer extends DexIndexedConsumer.ForwardingConsumer {
+
+    private byte[] bytes = null;
+
+    public SingleDexFileConsumer(DexIndexedConsumer consumer) {
+      super(consumer);
+    }
+
+    @Override
+    public void accept(
+        int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) {
+      if (fileIndex > 0) {
+        throw new CompilationError(
+            "Compilation result could not fit into a single dex file. "
+                + "Reduce the input-program size or run with --multi-dex enabled");
+      }
+      assert bytes == null;
+      // Store a copy of the bytes as we may not assume the backing is valid after accept returns.
+      bytes = data.copyByteData();
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      if (bytes != null) {
+        super.accept(0, ByteDataView.of(bytes), null, handler);
+      }
+      super.finished(handler);
+    }
+  }
+
+  private static class NamedDexFileConsumer extends DexIndexedConsumer.ForwardingConsumer {
+    private final Path output;
+
+    public NamedDexFileConsumer(Path output) {
+      super(null);
+      this.output = output;
+    }
+
+    @Override
+    public void accept(
+        int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) {
+      StandardOpenOption[] options = {
+        StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING
+      };
+      try (OutputStream stream = new BufferedOutputStream(Files.newOutputStream(output, options))) {
+        stream.write(data.getBuffer(), data.getOffset(), data.getLength());
+      } catch (IOException e) {
+        handler.error(new ExceptionDiagnostic(e, new PathOrigin(output)));
+      }
+    }
+  }
+
+  private static class DexKeepClassesConsumer extends DexIndexedConsumer.ArchiveConsumer {
+
+    private final List<Path> inputs;
+
+    public DexKeepClassesConsumer(Path archive, List<Path> inputs) {
+      super(archive);
+      this.inputs = inputs;
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      try {
+        writeZipWithClasses(handler);
+      } catch (IOException e) {
+        handler.error(new ExceptionDiagnostic(e, getOrigin()));
+      }
+      super.finished(handler);
+    }
+
+    private void writeZipWithClasses(DiagnosticsHandler handler) throws IOException {
+      // For each input archive file, add all class files within.
+      for (Path input : inputs) {
+        if (FileUtils.isArchive(input)) {
+          try (ZipFile zipFile = FileUtils.createZipFile(input.toFile(), StandardCharsets.UTF_8)) {
+            final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+            while (entries.hasMoreElements()) {
+              ZipEntry entry = entries.nextElement();
+              if (ZipUtils.isClassFile(entry.getName())) {
+                try (InputStream entryStream = zipFile.getInputStream(entry)) {
+                  byte[] bytes = ByteStreams.toByteArray(entryStream);
+                  outputBuilder.addFile(entry.getName(), ByteDataView.of(bytes), handler);
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  static void printHelpOn(PrintStream sink) throws IOException {
+    sink.println(USAGE_HEADER);
+    new DxCompatOptions.Spec().parser.printHelpOn(sink);
+  }
+
+  private static void processPath(File file, List<Path> files) {
+    if (!file.exists()) {
+      throw new CompilationError("File does not exist: " + file);
+    }
+    if (file.isDirectory()) {
+      processDirectory(file, files);
+      return;
+    }
+    Path path = file.toPath();
+    if (FileUtils.isZipFile(path) || FileUtils.isJarFile(path) || FileUtils.isClassFile(path)) {
+      files.add(path);
+      return;
+    }
+    if (FileUtils.isApkFile(path)) {
+      throw new Unimplemented("apk files not yet supported");
+    }
+  }
+
+  private static void processDirectory(File directory, List<Path> files) {
+    assert directory.exists();
+    for (File file : directory.listFiles()) {
+      processPath(file, files);
+    }
+  }
+}
diff --git a/src/main/keep.txt b/src/main/keep.txt
index d52b8f0..b0c58c9 100644
--- a/src/main/keep.txt
+++ b/src/main/keep.txt
@@ -25,3 +25,6 @@
 
 # Compatibility command line program used by the Android Platform build.
 -keep public class com.android.tools.r8.compatproguard.CompatProguard { public static void main(java.lang.String[]); }
+
+# Compatibility command line program used by in google3.
+-keep public class com.android.tools.r8.compatdx.CompatDx { public static void main(java.lang.String[]); }
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
new file mode 100644
index 0000000..4ec0131
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
@@ -0,0 +1,216 @@
+// 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.compatdx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.StringUtils.BraceType;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class CompatDxTests {
+  private static final int MAX_METHOD_COUNT = Constants.U16BIT_MAX;
+
+  private static final String EXAMPLE_JAR_FILE1 = ToolHelper.EXAMPLES_BUILD_DIR + "arithmetic.jar";
+  private static final String EXAMPLE_JAR_FILE2 = ToolHelper.EXAMPLES_BUILD_DIR + "barray.jar";
+
+  private static final String NO_LOCALS = "--no-locals";
+  private static final String NO_POSITIONS = "--positions=none";
+  private static final String MULTIDEX = "--multi-dex";
+  private static final String NUM_THREADS_5 = "--num-threads=5";
+
+  @Rule
+  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+  @Test
+  public void noFilesTest() throws IOException {
+    runDexer("--no-files");
+  }
+
+  @Test
+  public void noOutputTest() throws IOException {
+    runDexerWithoutOutput(NO_POSITIONS, NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE1);
+  }
+
+  @Test
+  public void singleJarInputFile() throws IOException {
+    runDexer(NO_POSITIONS, NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE1);
+  }
+
+  @Test
+  public void multipleJarInputFiles() throws IOException {
+    runDexer(NO_POSITIONS, NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE1, EXAMPLE_JAR_FILE2);
+  }
+
+  @Test
+  public void outputZipFile() throws IOException {
+    runDexerWithOutput("foo.dex.zip", NO_POSITIONS, NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE1);
+  }
+
+  @Test
+  public void useMultipleThreads() throws IOException {
+    runDexer(NUM_THREADS_5, NO_POSITIONS, NO_LOCALS, EXAMPLE_JAR_FILE1);
+  }
+
+  @Test
+  public void withPositions() throws IOException {
+    runDexer(NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE1);
+  }
+
+  @Test
+  public void withLocals() throws IOException {
+    runDexer(NO_POSITIONS, MULTIDEX, EXAMPLE_JAR_FILE1);
+  }
+
+  @Test
+  public void withoutMultidex() throws IOException {
+    runDexer(NO_POSITIONS, NO_LOCALS, EXAMPLE_JAR_FILE1);
+  }
+
+  @Test
+  public void writeToNamedDexFile() throws IOException {
+    runDexerWithOutput("named-output.dex", EXAMPLE_JAR_FILE1);
+  }
+
+  @Test
+  public void keepClassesSingleDexTest() throws IOException {
+    runDexerWithOutput("out.zip", "--keep-classes", EXAMPLE_JAR_FILE1);
+  }
+
+  @Test
+  public void keepClassesMultiDexTest() throws IOException {
+    runDexerWithOutput("out.zip", "--keep-classes", "--multi-dex", EXAMPLE_JAR_FILE1);
+  }
+
+  @Test
+  public void ignoreDexInArchiveTest() throws IOException {
+    // Create a JAR with both a .class and a .dex file (the .dex file is just empty).
+    Path jarWithClassesAndDex = temp.newFile("test.jar").toPath();
+    Files.copy(Paths.get(EXAMPLE_JAR_FILE1), jarWithClassesAndDex,
+        StandardCopyOption.REPLACE_EXISTING);
+    URI uri = URI.create("jar:" + jarWithClassesAndDex.toUri());
+    FileSystem fileSystem = FileSystems.newFileSystem(uri, ImmutableMap.of("create", "true"));
+    Path dexFile = fileSystem.getPath("classes.dex");
+    Files.newOutputStream(dexFile, StandardOpenOption.CREATE).close();
+    fileSystem.close();
+
+    // Only test this with CompatDx, as dx does not like the empty .dex file.
+    List<String> d8Args =ImmutableList.of(
+        "--output=" + temp.newFolder("out").toString(), jarWithClassesAndDex.toString());
+    CompatDx.main(d8Args.toArray(StringUtils.EMPTY_ARRAY));
+  }
+
+  private void runDexer(String... args) throws IOException {
+    runDexerWithOutput("", args);
+  }
+
+  private void runDexerWithoutOutput(String... args) throws IOException {
+    runDexerWithOutput(null, args);
+  }
+
+  private Path getOutputD8() {
+    return temp.getRoot().toPath().resolve("d8-out");
+  }
+
+  private Path getOutputDX() {
+    return temp.getRoot().toPath().resolve("dx-out");
+  }
+
+  private void runDexerWithOutput(String out, String... args) throws IOException {
+    Path d8Out = null;
+    Path dxOut = null;
+    if (out != null) {
+      Path baseD8 = getOutputD8();
+      Path baseDX = getOutputDX();
+      Files.createDirectory(baseD8);
+      Files.createDirectory(baseDX);
+      d8Out = baseD8.resolve(out);
+      dxOut = baseDX.resolve(out);
+      assertNotEquals(d8Out, dxOut);
+    }
+
+    List<String> d8Args = new ArrayList<>(args.length + 2);
+    d8Args.add("--dex");
+    if (d8Out != null) {
+      d8Args.add("--output=" + d8Out);
+    }
+    Collections.addAll(d8Args, args);
+    System.out.println("running: d8 " + StringUtils.join(d8Args, " "));
+    CompatDx.main(d8Args.toArray(StringUtils.EMPTY_ARRAY));
+
+    List<String> dxArgs = new ArrayList<>(args.length + 2);
+    if (dxOut != null) {
+      dxArgs.add("--output=" + dxOut);
+    }
+    Collections.addAll(dxArgs, args);
+    System.out.println("running: dx " + StringUtils.join(dxArgs, " "));
+    ProcessResult result = ToolHelper.runDX(dxArgs.toArray(StringUtils.EMPTY_ARRAY));
+    assertEquals(result.stderr, 0, result.exitCode);
+
+    if (out == null) {
+      // Can't check output if explicitly not writing any.
+      return;
+    }
+
+    List<Path> d8Files = Files.list(Files.isDirectory(d8Out) ? d8Out : d8Out.getParent())
+        .sorted().collect(Collectors.toList());
+    List<Path> dxFiles = Files.list(Files.isDirectory(dxOut) ? dxOut : dxOut.getParent())
+        .sorted().collect(Collectors.toList());
+    assertEquals("Out file names differ",
+        StringUtils.join(dxFiles, "\n", BraceType.NONE, (file) ->
+            file.getFileName().toString()),
+        StringUtils.join(d8Files, "\n", BraceType.NONE, (file) ->
+            file.getFileName().toString()));
+
+    for (int i = 0; i < d8Files.size(); i++) {
+      if (FileUtils.isArchive(d8Files.get(i))) {
+        compareArchiveFiles(d8Files.get(i), dxFiles.get(i));
+      }
+    }
+  }
+
+  private void compareArchiveFiles(Path d8File, Path dxFile) throws IOException {
+    ZipFile d8Zip = new ZipFile(d8File.toFile(), StandardCharsets.UTF_8);
+    ZipFile dxZip = new ZipFile(dxFile.toFile(), StandardCharsets.UTF_8);
+    // TODO(zerny): This should test resource containment too once supported.
+    Set<String> d8Content = d8Zip.stream().map(ZipEntry::getName).collect(Collectors.toSet());
+    Set<String> dxContent = dxZip.stream().map(ZipEntry::getName).collect(Collectors.toSet());
+    for (String entry : d8Content) {
+      assertTrue("Expected dx output to contain " + entry, dxContent.contains(entry));
+    }
+    for (String entry : dxContent) {
+      Path path = Paths.get(entry);
+      if (FileUtils.isDexFile(path) || FileUtils.isClassFile(path)) {
+        assertTrue("Expected d8 output to contain " + entry, d8Content.contains(entry));
+      }
+    }
+  }
+}