Reapply "Add a separate tool for computing the main-dex list"
This reverts commit 34247e0126ab7b46c3c7d8e1fc491ca1ad7cfb84
and fixes merge issues.
Bug: 65043103
Change-Id: Ie12e031390a735f6ca133739ef2c2bdabc130353
diff --git a/build.gradle b/build.gradle
index 7362238..82feb00 100644
--- a/build.gradle
+++ b/build.gradle
@@ -435,6 +435,20 @@
}
}
+task maindex(type: Jar) {
+ from sourceSets.main.output
+ baseName 'maindex'
+ manifest {
+ attributes 'Main-Class': 'com.android.tools.r8.GenerateMainDexList'
+ }
+ // In order to build without dependencies, pass the exclude_deps property using:
+ // gradle -Pexclude_deps maindex
+ if (!project.hasProperty('exclude_deps')) {
+ // Also include dependencies
+ from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
+ }
+}
+
task ExtractMarker(type: Jar) {
from sourceSets.main.output
baseName 'extractmarker'
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java
index 9ddbd9e..cbd8dd6 100644
--- a/src/main/java/com/android/tools/r8/BaseCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -3,51 +3,33 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8;
-import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.OutputMode;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
+/**
+ * Base class for commands and command builders for applications/tools which take an Android
+ * application (and a main-dex list) as input.
+ */
abstract class BaseCommand {
private final boolean printHelp;
private final boolean printVersion;
private final AndroidApp app;
- private final Path outputPath;
- private final OutputMode outputMode;
- private final CompilationMode mode;
- private final int minApiLevel;
BaseCommand(boolean printHelp, boolean printVersion) {
this.printHelp = printHelp;
this.printVersion = printVersion;
// All other fields are initialized with stub/invalid values.
this.app = null;
- this.outputPath = null;
- this.outputMode = OutputMode.Indexed;
- this.mode = null;
- this.minApiLevel = 0;
}
- BaseCommand(
- AndroidApp app,
- Path outputPath,
- OutputMode outputMode,
- CompilationMode mode,
- int minApiLevel) {
+ BaseCommand(AndroidApp app) {
assert app != null;
- assert mode != null;
- assert minApiLevel > 0;
this.app = app;
- this.outputPath = outputPath;
- this.outputMode = outputMode;
- this.mode = mode;
- this.minApiLevel = minApiLevel;
// Print options are not set.
printHelp = false;
printVersion = false;
@@ -69,52 +51,27 @@
// Internal access to the internal options.
abstract InternalOptions getInternalOptions();
- public Path getOutputPath() {
- return outputPath;
- }
-
- public CompilationMode getMode() {
- return mode;
- }
-
- public int getMinApiLevel() {
- return minApiLevel;
- }
-
- public OutputMode getOutputMode() {
- return outputMode;
- }
-
- abstract static class Builder<C extends BaseCommand, B extends Builder<C, B>> {
+ abstract public static class Builder<C extends BaseCommand, B extends Builder<C, B>> {
private boolean printHelp = false;
private boolean printVersion = false;
private final AndroidApp.Builder app;
- private Path outputPath = null;
- private OutputMode outputMode = OutputMode.Indexed;
- private CompilationMode mode;
- private int minApiLevel = Constants.DEFAULT_ANDROID_API;
- // Internal flag used by CompatDx to ignore dex files in archives.
- protected boolean ignoreDexInArchive = false;
-
- protected Builder(CompilationMode mode) {
- this(AndroidApp.builder(), mode, false);
+ protected Builder() {
+ this(AndroidApp.builder(), false);
}
- protected Builder(CompilationMode mode, boolean ignoreDexInArchive) {
- this(AndroidApp.builder(), mode, ignoreDexInArchive);
+ protected Builder(boolean ignoreDexInArchive) {
+ this(AndroidApp.builder(), ignoreDexInArchive);
}
// Internal constructor for testing.
Builder(AndroidApp app, CompilationMode mode) {
- this(AndroidApp.builder(app), mode, false);
+ this(AndroidApp.builder(app), false);
}
- private Builder(AndroidApp.Builder builder, CompilationMode mode, boolean ignoreDexInArchive) {
- assert mode != null;
+ protected Builder(AndroidApp.Builder builder, boolean ignoreDexInArchive) {
this.app = builder;
- this.mode = mode;
app.setIgnoreDexInArchive(ignoreDexInArchive);
}
@@ -181,52 +138,6 @@
return self();
}
- /** Get current compilation mode. */
- public CompilationMode getMode() {
- return mode;
- }
-
- /** Set compilation mode. */
- public B setMode(CompilationMode mode) {
- assert mode != null;
- this.mode = mode;
- return self();
- }
-
- /** Get the output path. Null if not set. */
- public Path getOutputPath() {
- return outputPath;
- }
-
- /** Get the output mode. */
- public OutputMode getOutputMode() {
- return outputMode;
- }
-
- /** Set an output path. Must be an existing directory or a zip file. */
- public B setOutputPath(Path outputPath) {
- this.outputPath = outputPath;
- return self();
- }
-
- /** Set an output mode. */
- public B setOutputMode(OutputMode outputMode) {
- this.outputMode = outputMode;
- return self();
- }
-
- /** Get the minimum API level (aka SDK version). */
- public int getMinApiLevel() {
- return minApiLevel;
- }
-
- /** Set the minimum required API level (aka SDK version). */
- public B setMinApiLevel(int minApiLevel) {
- assert minApiLevel > 0;
- this.minApiLevel = minApiLevel;
- return self();
- }
-
/**
* Add main-dex list files.
*
@@ -300,11 +211,7 @@
}
protected void validate() throws CompilationException {
- if (app.hasMainDexList() && outputMode == OutputMode.FilePerClass) {
- throw new CompilationException(
- "Option --main-dex-list cannot be used with --file-per-class");
- }
- FileUtils.validateOutputFile(outputPath);
+ // Currently does nothing.
}
}
}
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
new file mode 100644
index 0000000..5e8f975
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -0,0 +1,146 @@
+// 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.dex.Constants;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.OutputMode;
+import java.nio.file.Path;
+
+/**
+ * Base class for commands and command builders for compiler applications/tools which besides an
+ * Android application (and a main-dex list) also takes compilation output, compilation mode and
+ * min API level as input.
+ */
+abstract class BaseCompilerCommand extends BaseCommand {
+
+ private final Path outputPath;
+ private final OutputMode outputMode;
+ private final CompilationMode mode;
+ private final int minApiLevel;
+
+ BaseCompilerCommand(boolean printHelp, boolean printVersion) {
+ super(printHelp, printVersion);
+
+ this.outputPath = null;
+ this.outputMode = OutputMode.Indexed;
+ this.mode = null;
+ this.minApiLevel = 0;
+ }
+
+ BaseCompilerCommand(
+ AndroidApp app,
+ Path outputPath,
+ OutputMode outputMode,
+ CompilationMode mode,
+ int minApiLevel) {
+ super(app);
+ assert mode != null;
+ assert minApiLevel > 0;
+ this.outputPath = outputPath;
+ this.outputMode = outputMode;
+ this.mode = mode;
+ this.minApiLevel = minApiLevel;
+ }
+
+ public Path getOutputPath() {
+ return outputPath;
+ }
+
+ public CompilationMode getMode() {
+ return mode;
+ }
+
+ public int getMinApiLevel() {
+ return minApiLevel;
+ }
+
+ public OutputMode getOutputMode() {
+ return outputMode;
+ }
+
+ abstract public static class Builder<C extends BaseCompilerCommand, B extends Builder<C, B>>
+ extends BaseCommand.Builder<C, B> {
+
+ private Path outputPath = null;
+ private OutputMode outputMode = OutputMode.Indexed;
+ private CompilationMode mode;
+ private int minApiLevel = Constants.DEFAULT_ANDROID_API;
+
+ protected Builder(CompilationMode mode) {
+ this(AndroidApp.builder(), mode, false);
+ }
+
+ protected Builder(CompilationMode mode, boolean ignoreDexInArchive) {
+ this(AndroidApp.builder(), mode, ignoreDexInArchive);
+ }
+
+ // Internal constructor for testing.
+ Builder(AndroidApp app, CompilationMode mode) {
+ this(AndroidApp.builder(app), mode, false);
+ }
+
+ private Builder(AndroidApp.Builder builder, CompilationMode mode, boolean ignoreDexInArchive) {
+ super(builder, ignoreDexInArchive);
+ assert mode != null;
+ this.mode = mode;
+ }
+
+ /** Get current compilation mode. */
+ public CompilationMode getMode() {
+ return mode;
+ }
+
+ /** Set compilation mode. */
+ public B setMode(CompilationMode mode) {
+ assert mode != null;
+ this.mode = mode;
+ return self();
+ }
+
+ /** Get the output path. Null if not set. */
+ public Path getOutputPath() {
+ return outputPath;
+ }
+
+ /** Get the output mode. */
+ public OutputMode getOutputMode() {
+ return outputMode;
+ }
+
+ /** Set an output path. Must be an existing directory or a zip file. */
+ public B setOutputPath(Path outputPath) {
+ this.outputPath = outputPath;
+ return self();
+ }
+
+ /** Set an output mode. */
+ public B setOutputMode(OutputMode outputMode) {
+ this.outputMode = outputMode;
+ return self();
+ }
+
+ /** Get the minimum API level (aka SDK version). */
+ public int getMinApiLevel() {
+ return minApiLevel;
+ }
+
+ /** Set the minimum required API level (aka SDK version). */
+ public B setMinApiLevel(int minApiLevel) {
+ assert minApiLevel > 0;
+ this.minApiLevel = minApiLevel;
+ return self();
+ }
+
+ protected void validate() throws CompilationException {
+ super.validate();
+ if (getAppBuilder().hasMainDexList() && outputMode == OutputMode.FilePerClass) {
+ throw new CompilationException(
+ "Option --main-dex-list cannot be used with --file-per-class");
+ }
+ FileUtils.validateOutputFile(outputPath);
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 3618ab5..5b2e5cf 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -27,12 +27,12 @@
* .build();
* </pre>
*/
-public class D8Command extends BaseCommand {
+public class D8Command extends BaseCompilerCommand {
/**
* Builder for constructing a D8Command.
*/
- public static class Builder extends BaseCommand.Builder<D8Command, Builder> {
+ public static class Builder extends BaseCompilerCommand.Builder<D8Command, Builder> {
private boolean intermediate = false;
diff --git a/src/main/java/com/android/tools/r8/DexSegments.java b/src/main/java/com/android/tools/r8/DexSegments.java
index 4e4a19b..c27deb5 100644
--- a/src/main/java/com/android/tools/r8/DexSegments.java
+++ b/src/main/java/com/android/tools/r8/DexSegments.java
@@ -7,11 +7,9 @@
import com.android.tools.r8.dex.Segment;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.OutputMode;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Closer;
import java.io.IOException;
-import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
@@ -22,10 +20,6 @@
public static class Builder
extends BaseCommand.Builder<Command, Builder> {
- private Builder() {
- super(CompilationMode.RELEASE);
- }
-
@Override
Command.Builder self() {
return this;
@@ -38,8 +32,7 @@
return new Command(isPrintHelp());
}
validate();
- return new Command(
- getAppBuilder().build(), getOutputPath(), getOutputMode(), getMode(), getMinApiLevel());
+ return new Command(getAppBuilder().build());
}
}
@@ -77,13 +70,8 @@
}
}
- private Command(
- AndroidApp inputApp,
- Path outputPath,
- OutputMode outputMode,
- CompilationMode mode,
- int minApiLevel) {
- super(inputApp, outputPath, outputMode, mode, minApiLevel);
+ private Command(AndroidApp inputApp) {
+ super(inputApp);
}
private Command(boolean printHelp) {
diff --git a/src/main/java/com/android/tools/r8/Disassemble.java b/src/main/java/com/android/tools/r8/Disassemble.java
index a7bfdc9..bb72d5d 100644
--- a/src/main/java/com/android/tools/r8/Disassemble.java
+++ b/src/main/java/com/android/tools/r8/Disassemble.java
@@ -5,7 +5,6 @@
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.OutputMode;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.file.Path;
@@ -15,13 +14,13 @@
public class Disassemble {
public static class DisassembleCommand extends BaseCommand {
+ private final Path outputPath;
+
public static class Builder
extends BaseCommand.Builder<DisassembleCommand, DisassembleCommand.Builder> {
- private boolean useSmali = false;
- private Builder() {
- super(CompilationMode.RELEASE);
- }
+ private Path outputPath = null;
+ private boolean useSmali = false;
@Override
DisassembleCommand.Builder self() {
@@ -33,6 +32,15 @@
return this;
}
+ public Path getOutputPath() {
+ return outputPath;
+ }
+
+ public DisassembleCommand.Builder setOutputPath(Path outputPath) {
+ this.outputPath = outputPath;
+ return this;
+ }
+
public DisassembleCommand.Builder setUseSmali(boolean useSmali) {
this.useSmali = useSmali;
return this;
@@ -46,13 +54,7 @@
}
validate();
- return new DisassembleCommand(
- getAppBuilder().build(),
- getOutputPath(),
- getOutputMode(),
- getMode(),
- getMinApiLevel(),
- useSmali);
+ return new DisassembleCommand(getAppBuilder().build(), getOutputPath(), useSmali);
}
}
@@ -93,6 +95,9 @@
builder.setUseSmali(true);
} else if (arg.equals("--pg-map")) {
builder.setProguardMapFile(Paths.get(args[++i]));
+ } else if (arg.equals("--output")) {
+ String outputPath = args[++i];
+ builder.setOutputPath(Paths.get(outputPath));
} else {
if (arg.startsWith("--")) {
throw new CompilationException("Unknown option: " + arg);
@@ -102,23 +107,22 @@
}
}
- private DisassembleCommand(
- AndroidApp inputApp,
- Path outputPath,
- OutputMode outputMode,
- CompilationMode mode,
- int minApiLevel,
- boolean useSmali) {
- super(inputApp, outputPath, outputMode, mode, minApiLevel);
- assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
+ private DisassembleCommand(AndroidApp inputApp, Path outputPath, boolean useSmali) {
+ super(inputApp);
+ this.outputPath = outputPath;
this.useSmali = useSmali;
}
private DisassembleCommand(boolean printHelp, boolean printVersion) {
super(printHelp, printVersion);
+ this.outputPath = null;
this.useSmali = false;
}
+ public Path getOutputPath() {
+ return outputPath;
+ }
+
public boolean useSmali() {
return useSmali;
}
diff --git a/src/main/java/com/android/tools/r8/ExtractMarker.java b/src/main/java/com/android/tools/r8/ExtractMarker.java
index 0db183f..d798781 100644
--- a/src/main/java/com/android/tools/r8/ExtractMarker.java
+++ b/src/main/java/com/android/tools/r8/ExtractMarker.java
@@ -3,18 +3,14 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8;
-import com.google.common.collect.ImmutableList;
-
import com.android.tools.r8.dex.ApplicationReader;
import com.android.tools.r8.dex.Marker;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.OutputMode;
import com.android.tools.r8.utils.Timing;
-
+import com.google.common.collect.ImmutableList;
import java.io.IOException;
-import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ExecutionException;
@@ -24,10 +20,6 @@
public static class Builder
extends BaseCommand.Builder<ExtractMarker.Command, ExtractMarker.Command.Builder> {
- private Builder() {
- super(CompilationMode.RELEASE);
- }
-
@Override
ExtractMarker.Command.Builder self() {
return this;
@@ -40,8 +32,7 @@
return new ExtractMarker.Command(isPrintHelp());
}
validate();
- return new ExtractMarker.Command(
- getAppBuilder().build(), getOutputPath(), getOutputMode(), getMode(), getMinApiLevel());
+ return new ExtractMarker.Command(getAppBuilder().build());
}
}
@@ -79,13 +70,8 @@
}
}
- private Command(
- AndroidApp inputApp,
- Path outputPath,
- OutputMode outputMode,
- CompilationMode mode,
- int minApiLevel) {
- super(inputApp, outputPath, outputMode, mode, minApiLevel);
+ private Command(AndroidApp inputApp) {
+ super(inputApp);
}
private Command(boolean printHelp) {
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexList.java b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
new file mode 100644
index 0000000..44d523e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
@@ -0,0 +1,124 @@
+// 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.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.MainDexListBuilder;
+import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.shaking.RootSetBuilder;
+import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
+
+public class GenerateMainDexList {
+ private static final String VERSION = "v0.2.0";
+ private final Timing timing = new Timing("maindex");
+ private final InternalOptions options;
+
+ private GenerateMainDexList(InternalOptions options) {
+ this.options = options;
+ }
+
+ private List<String> run(AndroidApp app) throws IOException, ExecutionException {
+ ExecutorService executor = ThreadUtils.getExecutorService(options);
+ DexApplication application = new ApplicationReader(app, options, timing).read(executor);
+ AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
+ RootSet mainDexRootSet =
+ new RootSetBuilder(application, appInfo, options.mainDexKeepRules).run(executor);
+ Set<DexType> mainDexBaseClasses = new Enqueuer(appInfo).traceMainDex(mainDexRootSet, timing);
+ Set<DexType> mainDexClasses = new MainDexListBuilder(mainDexBaseClasses, application).run();
+
+ List<String> result = mainDexClasses.stream()
+ .map(c -> c.toSourceString().replace('.', '/') + ".class")
+ .sorted()
+ .collect(Collectors.toList());
+
+ if (options.printMainDexListFile != null) {
+ try (OutputStream mainDexOut = Files.newOutputStream(options.printMainDexListFile,
+ StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
+ PrintWriter writer = new PrintWriter(mainDexOut);
+ result.forEach(writer::println);
+ writer.flush();
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Main API entry for computing the main-dex list.
+ *
+ * The main-dex list is represented as a list of strings, each string specifies one class to
+ * keep in the primary dex file (<code>classes.dex</code>).
+ *
+ * A class is specified using the following format: "com/example/MyClass.class". That is
+ * "/" as separator between package components, and a trailing ".class".
+ *
+ * @param command main dex-list generator command.
+ * @return classes to keep in the primary dex file.
+ */
+ public static List<String> run(GenerateMainDexListCommand command)
+ throws IOException, ExecutionException {
+ ExecutorService executorService = ThreadUtils.getExecutorService(command.getInternalOptions());
+ try {
+ return run(command, executorService);
+ } finally {
+ executorService.shutdown();
+ }
+ }
+
+ /**
+ * Main API entry for computing the main-dex list.
+ *
+ * The main-dex list is represented as a list of strings, each string specifies one class to
+ * keep in the primary dex file (<code>classes.dex</code>).
+ *
+ * A class is specified using the following format: "com/example/MyClass.class". That is
+ * "/" as separator between package components, and a trailing ".class".
+ *
+ * @param command main dex-list generator command.
+ * @param executor executor service from which to get threads for multi-threaded processing.
+ * @return classes to keep in the primary dex file.
+ */
+ public static List<String> run(GenerateMainDexListCommand command, ExecutorService executor)
+ throws IOException, ExecutionException {
+ AndroidApp app = command.getInputApp();
+ InternalOptions options = command.getInternalOptions();
+ return new GenerateMainDexList(options).run(app);
+ }
+
+ public static void main(String[] args)
+ throws IOException, ProguardRuleParserException, CompilationException, ExecutionException {
+ GenerateMainDexListCommand.Builder builder = GenerateMainDexListCommand.parse(args);
+ GenerateMainDexListCommand command = builder.build();
+ if (command.isPrintHelp()) {
+ System.out.println(GenerateMainDexListCommand.USAGE_MESSAGE);
+ return;
+ }
+ if (command.isPrintVersion()) {
+ System.out.println("MainDexListGenerator " + VERSION);
+ return;
+ }
+ List<String> result = run(command);
+ if (command.getMainDexListOutputPath() == null) {
+ result.forEach(System.out::println);
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
new file mode 100644
index 0000000..76d7d56
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
@@ -0,0 +1,200 @@
+// 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.graph.DexItemFactory;
+import com.android.tools.r8.shaking.ProguardConfigurationParser;
+import com.android.tools.r8.shaking.ProguardConfigurationRule;
+import com.android.tools.r8.shaking.ProguardConfigurationSource;
+import com.android.tools.r8.shaking.ProguardConfigurationSourceFile;
+import com.android.tools.r8.shaking.ProguardConfigurationSourceStrings;
+import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GenerateMainDexListCommand extends BaseCommand {
+
+ private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
+ private final Path mainDexListOutput;
+ private final DexItemFactory factory;
+
+ /**
+ * Get the output path for the main-dex list. Null if not set.
+ */
+ public Path getMainDexListOutputPath() {
+ return mainDexListOutput;
+ }
+
+ public static class Builder extends BaseCommand.Builder<GenerateMainDexListCommand, Builder> {
+
+ private final DexItemFactory factory = new DexItemFactory();
+ private final List<ProguardConfigurationSource> mainDexRules = new ArrayList<>();
+ private Path mainDexListOutput = null;
+
+ @Override
+ GenerateMainDexListCommand.Builder self() {
+ return this;
+ }
+
+ /**
+ * Add proguard configuration file resources for automatic main dex list calculation.
+ */
+ public GenerateMainDexListCommand.Builder addMainDexRulesFiles(Path... paths) {
+ for (Path path : paths) {
+ mainDexRules.add(new ProguardConfigurationSourceFile(path));
+ }
+ return self();
+ }
+
+ /**
+ * Add proguard configuration file resources for automatic main dex list calculation.
+ */
+ public GenerateMainDexListCommand.Builder addMainDexRulesFiles(List<Path> paths) {
+ for (Path path : paths) {
+ mainDexRules.add(new ProguardConfigurationSourceFile(path));
+ }
+ return self();
+ }
+
+ /**
+ * Add proguard configuration for automatic main dex list calculation.
+ */
+ public GenerateMainDexListCommand.Builder addMainDexRules(List<String> lines) {
+ mainDexRules.add(new ProguardConfigurationSourceStrings(lines));
+ return self();
+ }
+
+ /**
+ * Get the output path for the main-dex list. Null if not set.
+ */
+ public Path getMainDexListOutputPath() {
+ return mainDexListOutput;
+ }
+
+ /**
+ * Set the output file for the main-dex list.
+ *
+ * If the file exists it will be overwritten.
+ */
+ public GenerateMainDexListCommand.Builder setMainDexListOutputPath(Path mainDexListOutputPath) {
+ mainDexListOutput = mainDexListOutputPath;
+ return self();
+ }
+
+
+ @Override
+ public GenerateMainDexListCommand build() throws CompilationException, IOException {
+ // If printing versions ignore everything else.
+ if (isPrintHelp() || isPrintVersion()) {
+ return new GenerateMainDexListCommand(isPrintHelp(), isPrintVersion());
+ }
+
+ validate();
+ ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
+ if (this.mainDexRules.isEmpty()) {
+ mainDexKeepRules = ImmutableList.of();
+ } else {
+ ProguardConfigurationParser parser = new ProguardConfigurationParser(factory);
+ try {
+ parser.parse(mainDexRules);
+ } catch (ProguardRuleParserException e) {
+ throw new CompilationException(e.getMessage(), e.getCause());
+ }
+ mainDexKeepRules = parser.getConfig().getRules();
+ }
+
+ return new GenerateMainDexListCommand(
+ factory,
+ getAppBuilder().build(),
+ mainDexKeepRules,
+ mainDexListOutput);
+ }
+ }
+
+ static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of(
+ "Usage: maindex [options] <input-files>",
+ " where <input-files> are JAR files",
+ " and options are:",
+ " --main-dex-rules <file> # Proguard keep rules for classes to place in the",
+ " # primary dex file.",
+ " --main-dex-list <file> # List of classes to place in the primary dex file.",
+ " --main-dex-list-output <file> # Output the full main-dex list in <file>.",
+ " --version # Print the version.",
+ " --help # Print this message."));
+
+
+ public static GenerateMainDexListCommand.Builder builder() {
+ return new GenerateMainDexListCommand.Builder();
+ }
+
+ public static GenerateMainDexListCommand.Builder parse(String[] args)
+ throws CompilationException, IOException {
+ GenerateMainDexListCommand.Builder builder = builder();
+ parse(args, builder);
+ return builder;
+ }
+
+ private static void parse(String[] args, GenerateMainDexListCommand.Builder builder)
+ throws CompilationException, IOException {
+ for (int i = 0; i < args.length; i++) {
+ String arg = args[i].trim();
+ if (arg.length() == 0) {
+ continue;
+ } else if (arg.equals("--help")) {
+ builder.setPrintHelp(true);
+ } else if (arg.equals("--version")) {
+ builder.setPrintVersion(true);
+ } else if (arg.equals("--main-dex-rules")) {
+ builder.addMainDexRulesFiles(Paths.get(args[++i]));
+ } else if (arg.equals("--main-dex-list")) {
+ builder.addMainDexListFiles(Paths.get(args[++i]));
+ } else if (arg.equals("--main-dex-list-output")) {
+ builder.setMainDexListOutputPath(Paths.get(args[++i]));
+ } else {
+ if (arg.startsWith("--")) {
+ throw new CompilationException("Unknown option: " + arg);
+ }
+ builder.addProgramFiles(Paths.get(arg));
+ }
+ }
+ }
+
+ private GenerateMainDexListCommand(
+ DexItemFactory factory,
+ AndroidApp inputApp,
+ ImmutableList<ProguardConfigurationRule> mainDexKeepRules,
+ Path mainDexListOutput) {
+ super(inputApp);
+ this.factory = factory;
+ this.mainDexKeepRules = mainDexKeepRules;
+ this.mainDexListOutput = mainDexListOutput;
+ }
+
+ private GenerateMainDexListCommand(boolean printHelp, boolean printVersion) {
+ super(printHelp, printVersion);
+ this.factory = new DexItemFactory();
+ this.mainDexKeepRules = ImmutableList.of();
+ this.mainDexListOutput = null;
+ }
+
+ @Override
+ InternalOptions getInternalOptions() {
+ InternalOptions internal = new InternalOptions(factory);
+ internal.mainDexKeepRules = mainDexKeepRules;
+ if (mainDexListOutput != null) {
+ internal.printMainDexListFile = mainDexListOutput;
+ }
+ internal.minimalMainDex = internal.debug;
+ internal.removeSwitchMaps = false;
+ internal.inlineAccessors = false;
+ return internal;
+ }
+}
+
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 318b3e6..890d570 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -24,9 +24,9 @@
import java.util.Optional;
import java.util.function.Consumer;
-public class R8Command extends BaseCommand {
+public class R8Command extends BaseCompilerCommand {
- public static class Builder extends BaseCommand.Builder<R8Command, Builder> {
+ public static class Builder extends BaseCompilerCommand.Builder<R8Command, Builder> {
private final List<ProguardConfigurationSource> mainDexRules = new ArrayList<>();
private Path mainDexListOutput = null;
@@ -96,7 +96,7 @@
}
/**
- * Add proguard configuration file resources for automatic main dex list calculation.
+ * Add proguard configuration for automatic main dex list calculation.
*/
public Builder addMainDexRules(List<String> lines) {
mainDexRules.add(new ProguardConfigurationSourceStrings(lines));
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index 5e7b42d..4dd1880 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -28,6 +28,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
@@ -572,5 +573,29 @@
return Collections
.unmodifiableMap(dependentNoShrinking.getOrDefault(item, Collections.emptyMap()));
}
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("RootSet");
+
+ builder.append("\nnoShrinking: " + noShrinking.size());
+ builder.append("\nnoOptimization: " + noOptimization.size());
+ builder.append("\nnoObfuscation: " + noObfuscation.size());
+ builder.append("\nreasonAsked: " + reasonAsked.size());
+ builder.append("\nkeepPackageName: " + keepPackageName.size());
+ builder.append("\ncheckDiscarded: " + checkDiscarded.size());
+ builder.append("\nnoSideEffects: " + noSideEffects.size());
+ builder.append("\nassumedValues: " + assumedValues.size());
+ builder.append("\ndependentNoShrinking: " + dependentNoShrinking.size());
+
+ builder.append("\n\nNo Shrinking:");
+ noShrinking.keySet().stream()
+ .sorted(Comparator.comparing(DexItem::toSourceString))
+ .forEach(a -> builder
+ .append("\n").append(a.toSourceString()).append(" ").append(noShrinking.get(a)));
+ builder.append("\n");
+ return builder.toString();
+ }
}
}
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 93d2ee9..3767286 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -599,13 +599,31 @@
.toArray(new String[0]));
}
+ public static ProcessResult forkGenerateMainDexList(Path dir, List<String> args1, String... args2)
+ throws IOException, InterruptedException {
+ List<String> args = new ArrayList<>();
+ args.addAll(args1);
+ args.addAll(Arrays.asList(args2));
+ return forkJava(dir, GenerateMainDexList.class, args);
+ }
+
+ public static ProcessResult forkGenerateMainDexList(Path dir, String... args)
+ throws IOException, InterruptedException {
+ return forkJava(dir, GenerateMainDexList.class, args);
+ }
+
private static ProcessResult forkJava(Path dir, Class clazz, String... args)
throws IOException, InterruptedException {
+ return forkJava(dir, clazz, Arrays.asList(args));
+ }
+
+ private static ProcessResult forkJava(Path dir, Class clazz, List<String> args)
+ throws IOException, InterruptedException {
List<String> command = new ImmutableList.Builder<String>()
.add(getJavaExecutable())
.add("-cp").add(System.getProperty("java.class.path"))
.add(clazz.getCanonicalName())
- .addAll(Arrays.asList(args))
+ .addAll(args)
.build();
return runProcess(new ProcessBuilder(command).directory(dir.toFile()));
}
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
index c7e0c18..ba5092f 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
@@ -9,16 +9,18 @@
import static com.android.tools.r8.utils.FileUtils.ZIP_EXTENSION;
import com.android.tools.r8.CompilationResult;
+import com.android.tools.r8.GenerateMainDexList;
+import com.android.tools.r8.GenerateMainDexListCommand;
import com.android.tools.r8.R8Command;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
@@ -107,7 +109,6 @@
mainDexRules,
expectedMainDexList,
minSdk,
- R8Command.builder(),
(options) -> {
options.inlineAccessors = false;
});
@@ -120,48 +121,86 @@
Path mainDexRules,
Path expectedMainDexList,
int minSdk,
- R8Command.Builder builder,
Consumer<InternalOptions> optionsConsumer)
throws Throwable {
Path out = temp.getRoot().toPath().resolve(testName + ZIP_EXTENSION);
Path inputJar = Paths.get(buildDir, packageName + JAR_EXTENSION);
- builder.setMinApiLevel(minSdk);
try {
- R8Command command = builder
+ // Build main-dex list using GenerateMainDexList.
+ GenerateMainDexListCommand.Builder mdlCommandBuilder = GenerateMainDexListCommand.builder();
+ GenerateMainDexListCommand command2 = mdlCommandBuilder
.addProgramFiles(inputJar)
- .addLibraryFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION),
- Paths.get(ToolHelper.getAndroidJar(minSdk)))
+ .addProgramFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION))
+ .addMainDexRulesFiles(mainDexRules)
+ .build();
+ List<String> mainDexGeneratorMainDexList =
+ GenerateMainDexList.run(command2).stream()
+ .map(this::mainDexStringToDescriptor)
+ .sorted()
+ .collect(Collectors.toList());
+
+ // Build main-dex list using R8.
+ R8Command.Builder r8CommandBuilder = R8Command.builder();
+ R8Command command = r8CommandBuilder
+ .setMinApiLevel(minSdk)
+ .addProgramFiles(inputJar)
+ .addProgramFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION))
+ .addLibraryFiles(Paths.get(ToolHelper.getAndroidJar(minSdk)))
.setOutputPath(out)
.addMainDexRulesFiles(mainDexRules)
.build();
CompilationResult result = ToolHelper.runR8WithFullResult(command, optionsConsumer);
- List<String> resultMainDexList =
+ List<String> r8MainDexList =
result.dexApplication.mainDexList.stream()
.filter(dexType -> isApplicationClass(dexType, result))
.map(dexType -> dexType.descriptor.toString())
+ .sorted()
.collect(Collectors.toList());
- Collections.sort(resultMainDexList);
+
+ // Check that both generated lists are the same as the reference list, except for lambda
+ // classes which are only produced when running R8.
String[] refList = new String(Files.readAllBytes(
expectedMainDexList), StandardCharsets.UTF_8).split("\n");
+ int nonLambdaOffset = 0;
for (int i = 0; i < refList.length; i++) {
String reference = refList[i].trim();
- String computed = resultMainDexList.get(i);
- if (reference.contains("-$$Lambda$")) {
- // For lambda classes we check that there is a lambda class for the right containing
- // class. However, we do not check the hash for the generated lambda class. The hash
- // changes for different compiler versions because different compiler versions generate
- // different lambda implementation method names.
- reference = reference.substring(0, reference.lastIndexOf('$'));
- computed = computed.substring(0, computed.lastIndexOf('$'));
+ checkSameMainDexEntry(reference, r8MainDexList.get(i));
+ // The main dex list generator does not do any lambda desugaring.
+ if (!isLambda(reference)) {
+ checkSameMainDexEntry(reference, mainDexGeneratorMainDexList.get(i - nonLambdaOffset));
+ } else {
+ nonLambdaOffset++;
}
- Assert.assertEquals(reference, computed);
}
} catch (ExecutionException e) {
throw e.getCause();
}
}
+ private boolean isLambda(String mainDexEntry) {
+ return mainDexEntry.contains("-$$Lambda$");
+ }
+
+ private String mainDexStringToDescriptor(String mainDexString) {
+ final String CLASS_EXTENSION = ".class";
+ Assert.assertTrue(mainDexString.endsWith(CLASS_EXTENSION));
+ return DescriptorUtils.getDescriptorFromClassBinaryName(
+ mainDexString.substring(0, mainDexString.length() - CLASS_EXTENSION.length()));
+ }
+
+ private void checkSameMainDexEntry(String reference, String computed) {
+ if (isLambda(reference)) {
+ // For lambda classes we check that there is a lambda class for the right containing
+ // class. However, we do not check the hash for the generated lambda class. The hash
+ // changes for different compiler versions because different compiler versions generate
+ // different lambda implementation method names.
+ reference = reference.substring(0, reference.lastIndexOf('$'));
+ computed = computed.substring(0, computed.lastIndexOf('$'));
+ }
+ Assert.assertEquals(reference, computed);
+ }
+
private boolean isApplicationClass(DexType dexType, CompilationResult result) {
DexClass clazz = result.appInfo.definitionFor(dexType);
return clazz != null && clazz.isProgramClass();
diff --git a/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java b/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java
new file mode 100644
index 0000000..64e7a5b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java
@@ -0,0 +1,144 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.GenerateMainDexListCommand;
+import com.android.tools.r8.ToolHelper;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+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.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+public class GenerateMainDexListCommandTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+ @Test
+ public void emptyCommand() throws Throwable {
+ verifyEmptyCommand(GenerateMainDexListCommand.builder().build());
+ verifyEmptyCommand(parse());
+ verifyEmptyCommand(parse(""));
+ verifyEmptyCommand(parse("", ""));
+ verifyEmptyCommand(parse(" "));
+ verifyEmptyCommand(parse(" ", " "));
+ verifyEmptyCommand(parse("\t"));
+ verifyEmptyCommand(parse("\t", "\t"));
+ }
+
+ private void verifyEmptyCommand(GenerateMainDexListCommand command) throws IOException {
+ assertEquals(0, ToolHelper.getApp(command).getDexProgramResources().size());
+ assertEquals(0, ToolHelper.getApp(command).getClassProgramResources().size());
+ assertFalse(ToolHelper.getApp(command).hasMainDexListResources());
+ }
+
+ // Add the jars used in the com.android.tools.r8.maindexlist.MainDexTracingTest test.
+ private void addInputJarsToCommandLine(List<String> args) {
+ args.add(Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, "multidex001" + JAR_EXTENSION)
+ .toAbsolutePath().toString());
+ args.add(Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION)
+ .toAbsolutePath().toString());
+ }
+
+ // Add main-dex rules used in the com.android.tools.r8.maindexlist.MainDexTracingTest test.
+ private void addMainDexRuleToCommandLine(List<String> args) {
+ args.add("--main-dex-rules");
+ args.add(Paths.get(ToolHelper.EXAMPLES_DIR, "multidex", "main-dex-rules.txt")
+ .toAbsolutePath().toString());
+ }
+
+ @Test
+ public void defaultOutIsCwd() throws Throwable {
+ Path working = temp.getRoot().toPath();
+ String mainDexListOutput = "main-dex-list.txt";
+ Path output = working.resolve(mainDexListOutput);
+ assertFalse(Files.exists(output));
+ List<String> args = new ArrayList<>();
+ addInputJarsToCommandLine(args);
+ addMainDexRuleToCommandLine(args);
+ assertEquals(0, ToolHelper.forkGenerateMainDexList(
+ working, args, "--main-dex-list-output", mainDexListOutput).exitCode);
+ assertTrue(Files.exists(output));
+ assertTrue(Files.size(output) > 0);
+ }
+
+ @Test
+ public void validOutputPath() throws Throwable {
+ Path existingFile = temp.getRoot().toPath().resolve("existing_output");
+ try (OutputStream existingFileOut = Files.newOutputStream(existingFile,
+ StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
+ PrintWriter writer = new PrintWriter(existingFileOut);
+ writer.println("Hello, world!");
+ writer.flush();
+ }
+ Path nonExistingFile = temp.getRoot().toPath().resolve("non_existing_output");
+ assertEquals(
+ existingFile,
+ GenerateMainDexListCommand.builder().setMainDexListOutputPath(existingFile).build()
+ .getMainDexListOutputPath());
+ assertEquals(
+ nonExistingFile,
+ GenerateMainDexListCommand.builder().setMainDexListOutputPath(nonExistingFile).build()
+ .getMainDexListOutputPath());
+ assertEquals(
+ existingFile,
+ parse("--main-dex-list-output", existingFile.toString()).getMainDexListOutputPath());
+ assertEquals(
+ nonExistingFile,
+ parse("--main-dex-list-output", nonExistingFile.toString()).getMainDexListOutputPath());
+ }
+
+ @Test
+ public void nonExistingOutputFileInNonExistingDir() throws Throwable {
+ Path nonExistingFileInNonExistingDir =
+ temp.getRoot().toPath().resolve("a/path/that/does/not/exist");
+ assertEquals(
+ nonExistingFileInNonExistingDir,
+ GenerateMainDexListCommand.builder()
+ .setMainDexListOutputPath(nonExistingFileInNonExistingDir).build()
+ .getMainDexListOutputPath());
+ assertEquals(
+ nonExistingFileInNonExistingDir,
+ parse("--main-dex-list-output",
+ nonExistingFileInNonExistingDir.toString()).getMainDexListOutputPath());
+ }
+
+ @Test
+ public void mainDexRules() throws Throwable {
+ Path mainDexRules1 = temp.newFile("main-dex-1.rules").toPath();
+ Path mainDexRules2 = temp.newFile("main-dex-2.rules").toPath();
+ parse("--main-dex-rules", mainDexRules1.toString());
+ parse(
+ "--main-dex-rules", mainDexRules1.toString(), "--main-dex-rules", mainDexRules2.toString());
+ }
+
+ @Test
+ public void mainDexList() throws Throwable {
+ Path mainDexList1 = temp.newFile("main-dex-list-1.txt").toPath();
+ Path mainDexList2 = temp.newFile("main-dex-list-2.txt").toPath();
+ parse("--main-dex-list", mainDexList1.toString());
+ parse("--main-dex-list", mainDexList1.toString(), "--main-dex-list", mainDexList2.toString());
+ }
+
+ private GenerateMainDexListCommand parse(String... args) throws Throwable {
+ return GenerateMainDexListCommand.parse(args).build();
+ }
+}