blob: 7d2907a689ffbbf4270b2d2eddd073ac35525ce4 [file] [log] [blame]
// 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.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.File;
import java.io.IOException;
import java.io.InputStream;
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) {
Version.printToolVersion("CompatDx");
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, byte[] 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;
bytes = data;
}
@Override
public void finished(DiagnosticsHandler handler) {
if (bytes != null) {
super.accept(0, 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, byte[] data, Set<String> descriptors, DiagnosticsHandler handler) {
try {
Files.write(
output,
data,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE);
} 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 = new ZipFile(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)) {
outputBuilder.addFile(
entry.getName(), ByteStreams.toByteArray(entryStream), 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);
}
}
}