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));
+ }
+ }
+ }
+}