Merge "Added support for minimal main dex"
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 34834f2..06ebe60 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -26,6 +26,7 @@
public static class Builder extends BaseCommand.Builder<R8Command, Builder> {
private final List<Path> mainDexRules = new ArrayList<>();
+ private boolean minimalMainDex = false;
private final List<Path> proguardConfigFiles = new ArrayList<>();
private Optional<Boolean> treeShaking = Optional.empty();
private Optional<Boolean> minification = Optional.empty();
@@ -77,6 +78,16 @@
}
/**
+ * Request minimal main dex generated when main dex rules are used.
+ *
+ * The main purpose of this is to verify that the main dex rules are sufficient
+ * for running on a platform without native multi dex support.
+ */
+ public Builder setMinimalMainDex(boolean value) {
+ minimalMainDex = value;
+ return this;
+ }
+ /**
* Add proguard configuration file resources.
*/
public Builder addProguardConfigurationFiles(Path... paths) {
@@ -161,6 +172,7 @@
getOutputPath(),
getOutputMode(),
mainDexKeepRules,
+ minimalMainDex,
configuration,
getMode(),
getMinApiLevel(),
@@ -196,6 +208,7 @@
" --help # Print this message."));
private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
+ private final boolean minimalMainDex;
private final ProguardConfiguration proguardConfiguration;
private final boolean useTreeShaking;
private final boolean useMinification;
@@ -259,6 +272,8 @@
builder.setMinification(false);
} else if (arg.equals("--multidex-rules")) {
builder.addMainDexRules(Paths.get(args[++i]));
+ } else if (arg.equals("--minimal-maindex")) {
+ builder.setMinimalMainDex(true);
} else if (arg.equals("--pg-conf")) {
builder.addProguardConfigurationFiles(Paths.get(args[++i]));
} else if (arg.equals("--pg-map")) {
@@ -300,6 +315,7 @@
Path outputPath,
OutputMode outputMode,
ImmutableList<ProguardConfigurationRule> mainDexKeepRules,
+ boolean minimalMainDex,
ProguardConfiguration proguardConfiguration,
CompilationMode mode,
int minApiLevel,
@@ -311,6 +327,7 @@
assert mainDexKeepRules != null;
assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
this.mainDexKeepRules = mainDexKeepRules;
+ this.minimalMainDex = minimalMainDex;
this.proguardConfiguration = proguardConfiguration;
this.useTreeShaking = useTreeShaking;
this.useMinification = useMinification;
@@ -320,6 +337,7 @@
private R8Command(boolean printHelp, boolean printVersion) {
super(printHelp, printVersion);
mainDexKeepRules = ImmutableList.of();
+ minimalMainDex = false;
proguardConfiguration = null;
useTreeShaking = false;
useMinification = false;
@@ -376,6 +394,7 @@
internal.classObfuscationDictionary = proguardConfiguration.getClassObfuscationDictionary();
internal.obfuscationDictionary = proguardConfiguration.getObfuscationDictionary();
internal.mainDexKeepRules = mainDexKeepRules;
+ internal.minimalMainDex = minimalMainDex;
internal.keepRules = proguardConfiguration.getRules();
internal.dontWarnPatterns = proguardConfiguration.getDontWarnPatterns();
internal.outputMode = getOutputMode();
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 7476792..d7fb9f8 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -125,16 +125,20 @@
"Cannot combine package distribution definition with file-per-class option.";
distributor = new FilePerClassDistributor(this);
} else if (packageDistribution != null) {
+ assert !options.minimalMainDex :
+ "Cannot combine package distribution definition with minimal-main-dex option.";
distributor = new PackageMapDistributor(this, packageDistribution, executorService);
} else {
- distributor = new FillFilesDistributor(this);
+ distributor = new FillFilesDistributor(this, options.minimalMainDex);
}
Map<Integer, VirtualFile> newFiles = distributor.run();
- // Write the dex files and the Proguard mapping file in parallel.
+ // Write the dex files and the Proguard mapping file in parallel. Use a linked hash map
+ // as the order matters when addDexProgramData is called below.
LinkedHashMap<VirtualFile, Future<byte[]>> dexDataFutures = new LinkedHashMap<>();
- for (Integer index : newFiles.keySet()) {
- VirtualFile newFile = newFiles.get(index);
+ for (int i = 0; i < newFiles.size(); i++) {
+ VirtualFile newFile = newFiles.get(i);
+ assert newFile.getId() == i;
if (!newFile.isEmpty()) {
dexDataFutures.put(newFile, executorService.submit(() -> writeDexFile(newFile)));
}
diff --git a/src/main/java/com/android/tools/r8/dex/Constants.java b/src/main/java/com/android/tools/r8/dex/Constants.java
index 201fc82..9527a97 100644
--- a/src/main/java/com/android/tools/r8/dex/Constants.java
+++ b/src/main/java/com/android/tools/r8/dex/Constants.java
@@ -11,6 +11,7 @@
public static final int ANDROID_O_API = 26;
public static final int ANDROID_N_API = 24;
public static final int ANDROID_K_API = 19;
+ public static final int ANDROID_L_API = 21;
public static final int DEFAULT_ANDROID_API = 1;
/** dex file version number for Android O (API level 26) */
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index 3db0502..55511fd 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -26,6 +26,7 @@
import com.android.tools.r8.utils.PackageDistribution;
import com.android.tools.r8.utils.ThreadUtils;
import com.google.common.collect.Iterators;
+import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.io.OutputStreamWriter;
@@ -53,9 +54,17 @@
public class VirtualFile {
+ // The fill strategy determine how to distribute classes into dex files.
enum FillStrategy {
+ // Only put classes matches by the main dex rules into the first dex file. Distribute remaining
+ // classes in additional dex files filling each dex file as much as possible.
+ MINIMAL_MAIN_DEX,
+ // Distribute classes in as few dex files as possible filling each dex file as much as possible.
FILL_MAX,
+ // Distribute classes keeping some space for future growth. This is mainly useful together with
+ // the package map distribution.
LEAVE_SPACE_FOR_GROWTH,
+ // TODO(sgjesse): Does "minimal main dex" combined with "leave space for growth" make sense?
}
private static final int MAX_ENTRIES = (Short.MAX_VALUE << 1) + 1;
@@ -75,6 +84,10 @@
this.transaction = new IndexedItemTransaction(indexedItems, namingLens);
}
+ public int getId() {
+ return id;
+ }
+
public Set<String> getClassDescriptors() {
Set<String> classDescriptors = new HashSet<>();
for (DexProgramClass clazz : indexedItems.classes) {
@@ -187,7 +200,7 @@
public abstract static class Distributor {
protected final DexApplication application;
protected final ApplicationWriter writer;
- protected final Map<Integer, VirtualFile> nameToFileMap = new LinkedHashMap<>();
+ protected final Map<Integer, VirtualFile> nameToFileMap = new HashMap<>();
public Distributor(ApplicationWriter writer) {
this.application = writer.application;
@@ -283,8 +296,11 @@
}
public static class FillFilesDistributor extends DistributorBase {
- public FillFilesDistributor(ApplicationWriter writer) {
+ private final FillStrategy fillStrategy;
+
+ public FillFilesDistributor(ApplicationWriter writer, boolean minimalMainDex) {
super(writer);
+ this.fillStrategy = minimalMainDex ? FillStrategy.MINIMAL_MAIN_DEX : FillStrategy.FILL_MAX;
}
public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
@@ -297,13 +313,13 @@
// First fill required classes into the main dex file.
fillForMainDexList(classes);
- // Sort the classes based on the original names.
+ // Sort the remaining classes based on the original names.
// This with make classes from the same package be adjacent.
classes = sortClassesByPackage(classes, originalNames);
new PackageSplitPopulator(
nameToFileMap, classes, originalNames, null, application.dexItemFactory,
- FillStrategy.FILL_MAX, writer.namingLens)
+ fillStrategy, writer.namingLens)
.call();
return nameToFileMap;
}
@@ -336,7 +352,7 @@
// First fill required classes into the main dex file.
fillForMainDexList(classes);
- // Sort the classes based on the original names.
+ // Sort the remaining classes based on the original names.
// This with make classes from the same package be adjacent.
classes = sortClassesByPackage(classes, originalNames);
@@ -712,6 +728,74 @@
}
/**
+ * Helper class to cycle through the set of virtual files.
+ *
+ * Iteration starts at the first file and iterates through all files.
+ *
+ * When {@link VirtualFileCycler#restart()} is called iteration of all files is restarted at the
+ * current file.
+ *
+ * If the fill strategy indicate that the main dex file should be minimal, then the main dex file
+ * will not be part of the iteration.
+ */
+ private static class VirtualFileCycler {
+ private Map<Integer, VirtualFile> files;
+ private final NamingLens namingLens;
+ private final FillStrategy fillStrategy;
+
+ private int nextFileId;
+ private Iterator<VirtualFile> allFilesCyclic;
+ private Iterator<VirtualFile> activeFiles;
+
+ VirtualFileCycler(Map<Integer, VirtualFile> files, NamingLens namingLens,
+ FillStrategy fillStrategy) {
+ this.files = files;
+ this.namingLens = namingLens;
+ this.fillStrategy = fillStrategy;
+
+ nextFileId = files.size();
+ if (fillStrategy == FillStrategy.MINIMAL_MAIN_DEX) {
+ // The main dex file is filtered out, so ensure at least one file for the remaining
+ // classes
+ files.put(nextFileId, new VirtualFile(nextFileId, namingLens));
+ this.files = Maps.filterKeys(files, key -> key != 0);
+ nextFileId++;
+ }
+
+ reset();
+ }
+
+ private void reset() {
+ allFilesCyclic = Iterators.cycle(files.values());
+ restart();
+ }
+
+ boolean hasNext() {
+ return activeFiles.hasNext();
+ }
+
+ VirtualFile next() {
+ VirtualFile next = activeFiles.next();
+ assert fillStrategy != FillStrategy.MINIMAL_MAIN_DEX || next.getId() != 0;
+ return next;
+ }
+
+ // Start a new iteration over all files, starting at the current one.
+ void restart() {
+ activeFiles = Iterators.limit(allFilesCyclic, files.size());
+ }
+
+ VirtualFile addFile() {
+ VirtualFile newFile = new VirtualFile(nextFileId, namingLens);
+ files.put(nextFileId, newFile);
+ nextFileId++;
+
+ reset();
+ return newFile;
+ }
+ }
+
+ /**
* Distributes the given classes over the files in package order.
*
* <p>The populator avoids package splits. Big packages are split into subpackages if their size
@@ -735,13 +819,12 @@
*/
private static final int MIN_FILL_FACTOR = 5;
- private final Map<Integer, VirtualFile> files;
private final List<DexProgramClass> classes;
private final Map<DexProgramClass, String> originalNames;
private final Set<String> previousPrefixes;
private final DexItemFactory dexItemFactory;
private final FillStrategy fillStrategy;
- private final NamingLens namingLens;
+ private final VirtualFileCycler cycler;
PackageSplitPopulator(
Map<Integer, VirtualFile> files,
@@ -751,13 +834,12 @@
DexItemFactory dexItemFactory,
FillStrategy fillStrategy,
NamingLens namingLens) {
- this.files = files;
this.classes = new ArrayList<>(classes);
this.originalNames = originalNames;
this.previousPrefixes = previousPrefixes;
this.dexItemFactory = dexItemFactory;
this.fillStrategy = fillStrategy;
- this.namingLens = namingLens;
+ this.cycler = new VirtualFileCycler(files, namingLens, fillStrategy);
}
private String getOriginalName(DexProgramClass clazz) {
@@ -766,14 +848,12 @@
@Override
public Map<String, Integer> call() throws IOException {
- Iterator<VirtualFile> allFilesCyclic = Iterators.cycle(files.values());
- Iterator<VirtualFile> activeFiles = Iterators.limit(allFilesCyclic, files.size());
int prefixLength = MINIMUM_PREFIX_LENGTH;
int transactionStartIndex = 0;
int fileStartIndex = 0;
String currentPrefix = null;
Map<String, Integer> newPackageAssignments = new LinkedHashMap<>();
- VirtualFile current = activeFiles.next();
+ VirtualFile current = cycler.next();
List<DexProgramClass> nonPackageClasses = new ArrayList<>();
for (int classIndex = 0; classIndex < classes.size(); classIndex++) {
DexProgramClass clazz = classes.get(classIndex);
@@ -781,8 +861,8 @@
if (!PackageMapPopulator.coveredByPrefix(originalName, currentPrefix)) {
if (currentPrefix != null) {
current.commitTransaction();
- // Reset the iterator to again iterate over all files, starting with the current one.
- activeFiles = Iterators.limit(allFilesCyclic, files.size());
+ // Reset the cycler to again iterate over all files, starting with the current one.
+ cycler.restart();
assert !newPackageAssignments.containsKey(currentPrefix);
newPackageAssignments.put(currentPrefix, current.id);
// Try to reduce the prefix length if possible. Only do this on a successful commit.
@@ -832,7 +912,7 @@
// The idea is that we do not increase the number of files, so it has to fit
// somewhere.
fileStartIndex = transactionStartIndex;
- if (!activeFiles.hasNext()) {
+ if (!cycler.hasNext()) {
// Special case where we simply will never be able to fit the current package into
// one dex file. This is currently the case for Strings in jumbo tests, see:
// b/33227518
@@ -843,11 +923,9 @@
transactionStartIndex = classIndex + 1;
}
// All files are filled up to the 20% mark.
- files.put(files.size(), new VirtualFile(files.size(), namingLens));
- allFilesCyclic = Iterators.cycle(files.values());
- activeFiles = Iterators.limit(allFilesCyclic, files.size());
+ cycler.addFile();
}
- current = activeFiles.next();
+ current = cycler.next();
}
currentPrefix = null;
// Go back to previous start index.
@@ -861,24 +939,25 @@
newPackageAssignments.put(currentPrefix, current.id);
}
if (nonPackageClasses.size() > 0) {
- addNonPackageClasses(Iterators.limit(allFilesCyclic, files.size()), nonPackageClasses);
+ addNonPackageClasses(cycler, nonPackageClasses);
}
return newPackageAssignments;
}
- private void addNonPackageClasses(Iterator<VirtualFile> activeFiles,
- List<DexProgramClass> nonPackageClasses) {
+ private void addNonPackageClasses(
+ VirtualFileCycler cycler, List<DexProgramClass> nonPackageClasses) {
+ cycler.restart();
VirtualFile current;
- current = activeFiles.next();
+ current = cycler.next();
for (DexProgramClass clazz : nonPackageClasses) {
if (current.isFilledEnough(fillStrategy)) {
- current = getVirtualFile(activeFiles);
+ current = getVirtualFile(cycler);
}
current.addClass(clazz);
while (current.isFull()) {
// This only happens if we have a huge class, that takes up more than 20% of a dex file.
current.abortTransaction();
- current = getVirtualFile(activeFiles);
+ current = getVirtualFile(cycler);
boolean wasEmpty = current.isEmpty();
current.addClass(clazz);
if (wasEmpty && current.isFull()) {
@@ -890,13 +969,12 @@
}
}
- private VirtualFile getVirtualFile(Iterator<VirtualFile> activeFiles) {
+ private VirtualFile getVirtualFile(VirtualFileCycler cycler) {
VirtualFile current = null;
- while (activeFiles.hasNext()
- && (current = activeFiles.next()).isFilledEnough(fillStrategy)) {}
+ while (cycler.hasNext()
+ && (current = cycler.next()).isFilledEnough(fillStrategy)) {}
if (current == null || current.isFilledEnough(fillStrategy)) {
- current = new VirtualFile(files.size(), namingLens);
- files.put(files.size(), current);
+ current = cycler.addFile();
}
return current;
}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 9d2379a..7b1cfe1 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -86,6 +86,7 @@
public List<String> obfuscationDictionary = ImmutableList.of();
public ImmutableList<ProguardConfigurationRule> mainDexKeepRules = ImmutableList.of();
+ public boolean minimalMainDex;
public ImmutableList<ProguardConfigurationRule> keepRules = ImmutableList.of();
public ImmutableSet<ProguardTypeMatcher> dontWarnPatterns = ImmutableSet.of();
diff --git a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
index 4131b86..bd97a3b 100644
--- a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
+++ b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
@@ -8,11 +8,15 @@
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.maindexlist.MainDexListTests;
+import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.OutputMode;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.StringUtils.BraceType;
+import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -21,6 +25,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
@@ -95,9 +100,14 @@
}
@Test
- public void singleDexProgramIsTooLarge() throws IOException {
+ public void singleDexProgramIsTooLarge() throws IOException, ExecutionException {
+ // Generate an application that will not fit into a single dex file.
+ AndroidApp generated = MainDexListTests.generateApplication(
+ ImmutableList.of("A", "B"), Constants.DEFAULT_ANDROID_API, Constants.U16BIT_MAX / 2 + 1);
+ Path applicationJar = temp.newFile("application.jar").toPath();
+ generated.write(applicationJar, OutputMode.Indexed, true);
thrown.expect(CompilationError.class);
- runDexer(MainDexListTests.getTwoLargeClassesAppPath().toString());
+ runDexer(applicationJar.toString());
}
@Test
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index f540bc0..4eb8545 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -3,17 +3,15 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.maindexlist;
-import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.tools.r8.CompilationException;
-import com.android.tools.r8.D8;
import com.android.tools.r8.D8Command;
import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.dex.ApplicationWriter;
import com.android.tools.r8.dex.Constants;
@@ -47,6 +45,7 @@
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ListUtils;
@@ -56,123 +55,100 @@
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
-import com.google.common.io.ByteStreams;
-import com.google.common.io.Closer;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
-public class MainDexListTests {
-
- private static final Path BASE =
- Paths.get("src", "test", "java", "com", "android", "tools", "r8", "maindexlist");
-
- private static boolean verifyApplications = true;
- private static boolean regenerateApplications = false;
+public class MainDexListTests extends TestBase {
private static final int MAX_METHOD_COUNT = Constants.U16BIT_MAX;
+
private static final List<String> TWO_LARGE_CLASSES = ImmutableList.of("A", "B");
- private static final String TWO_LARGE_CLASSES_APP = "two-large-classes.zip";
+ private static final int MANY_CLASSES_COUNT = 10000;
+ private static final int MANY_CLASSES_SINGLE_DEX_METHODS_PER_CLASS = 2;
+ private static final int MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS = 10;
+ private static List<String> MANY_CLASSES;
- private static final int MANY_CLASSES_COUNT = 1000;
- private static final List<String> MANY_CLASSES;
- private static final String MANY_CLASSES_APP = "many-classes.zip";
+ @ClassRule
+ public static TemporaryFolder generatedApplicationsFolder = new TemporaryFolder();
- static {
+ // Generate the test applications in a @BeforeClass method, as they are used by several tests.
+ @BeforeClass
+ public static void generateTestApplications() throws Throwable {
ImmutableList.Builder<String> builder = ImmutableList.builder();
for (int i = 0; i < MANY_CLASSES_COUNT; ++i) {
String pkg = i % 2 == 0 ? "a" : "b";
builder.add(pkg + ".Class" + i);
}
MANY_CLASSES = builder.build();
+
+ // Generates an application with many classes, every even in one package and every odd in
+ // another. Keep the number of methods low enough for single dex application.
+ AndroidApp generated = generateApplication(
+ MANY_CLASSES, Constants.DEFAULT_ANDROID_API, MANY_CLASSES_SINGLE_DEX_METHODS_PER_CLASS);
+ generated.write(getManyClassesSingleDexAppPath(), OutputMode.Indexed, false);
+
+ // Generates an application with many classes, every even in one package and every odd in
+ // another. Add enough methods so the application cannot fit into one dex file.
+ generated = generateApplication(
+ MANY_CLASSES, Constants.ANDROID_L_API, MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS);
+ generated.write(getManyClassesMultiDexAppPath(), OutputMode.Indexed, false);
+
+ // Generates an application with two classes, each with the maximum possible number of methods.
+ generated = generateApplication(TWO_LARGE_CLASSES, Constants.ANDROID_N_API, MAX_METHOD_COUNT);
+ generated.write(getTwoLargeClassesAppPath(), OutputMode.Indexed, false);
}
- public static Path getTwoLargeClassesAppPath() {
- return BASE.resolve(TWO_LARGE_CLASSES_APP);
+ private static Path getTwoLargeClassesAppPath() {
+ return generatedApplicationsFolder.getRoot().toPath().resolve("two-large-classes.zip");
}
- public static Path getManyClassesAppPath() {
- return BASE.resolve(MANY_CLASSES_APP);
+ private static Path getManyClassesSingleDexAppPath() {
+ return generatedApplicationsFolder.getRoot().toPath().resolve("many-classes-mono.zip");
}
- // Generates an application with two classes, each with the maximum possible number of methods.
- @Test
- public void generateTwoLargeClasses() throws IOException, ExecutionException {
- if (!verifyApplications && !regenerateApplications) {
- return;
- }
- AndroidApp generated = generateApplication(TWO_LARGE_CLASSES, MAX_METHOD_COUNT);
- if (regenerateApplications) {
- generated.write(getTwoLargeClassesAppPath(), OutputMode.Indexed, true);
- } else {
- AndroidApp cached = AndroidApp.fromProgramFiles(getTwoLargeClassesAppPath());
- compareToCachedVersion(cached, generated, TWO_LARGE_CLASSES_APP);
- }
+ private static Path getManyClassesMultiDexAppPath() {
+ return generatedApplicationsFolder.getRoot().toPath().resolve("many-classes-stereo.zip");
}
- // Generates an application with many classes, every even in one package and every odd in another.
- @Test
- public void generateManyClasses() throws IOException, ExecutionException {
- if (!verifyApplications && !regenerateApplications) {
- return;
- }
- AndroidApp generated = generateApplication(MANY_CLASSES, 1);
- if (regenerateApplications) {
- generated.write(getManyClassesAppPath(), OutputMode.Indexed, true);
- } else {
- AndroidApp cached = AndroidApp.fromProgramFiles(getManyClassesAppPath());
- compareToCachedVersion(cached, generated, MANY_CLASSES_APP);
- }
- }
-
- private static void compareToCachedVersion(AndroidApp cached, AndroidApp generated, String name)
- throws IOException {
- assertEquals("On-disk cached app (" + name + ") differs in file count from regeneration"
- + "Set 'regenerateApplications = true' and rerun this test to update the cache.",
- cached.getDexProgramResources().size(), generated.getDexProgramResources().size());
- try (Closer closer = Closer.create()) {
- for (int i = 0; i < cached.getDexProgramResources().size(); i++) {
- byte[] cachedBytes =
- ByteStreams.toByteArray(cached.getDexProgramResources().get(i).getStream(closer));
- byte[] generatedBytes =
- ByteStreams.toByteArray(generated.getDexProgramResources().get(i).getStream(closer));
- assertArrayEquals("On-disk cached app differs in byte content from regeneration"
- + "Set 'regenerateApplications = true' and rerun this test to update the cache.",
- cachedBytes, generatedBytes);
- }
- }
- }
-
- @Rule
- public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
-
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
+ public void checkGeneratedFileFitInSingleDexFile() {
+ assertTrue(MANY_CLASSES_COUNT * MANY_CLASSES_SINGLE_DEX_METHODS_PER_CLASS <= MAX_METHOD_COUNT);
+ }
+
+ @Test
+ public void checkGeneratedFileNeedsTwoDexFiles() {
+ assertTrue(MANY_CLASSES_COUNT * MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS > MAX_METHOD_COUNT);
+ }
+
+ @Test
public void putFirstClassInMainDexList() throws Throwable {
- verifyMainDexContains(TWO_LARGE_CLASSES.subList(0, 1), getTwoLargeClassesAppPath());
+ verifyMainDexContains(TWO_LARGE_CLASSES.subList(0, 1), getTwoLargeClassesAppPath(), false);
}
@Test
public void putSecondClassInMainDexList() throws Throwable {
- verifyMainDexContains(TWO_LARGE_CLASSES.subList(1, 2), getTwoLargeClassesAppPath());
+ verifyMainDexContains(TWO_LARGE_CLASSES.subList(1, 2), getTwoLargeClassesAppPath(), false);
}
@Test
public void cannotFitBothIntoMainDex() throws Throwable {
thrown.expect(CompilationError.class);
- verifyMainDexContains(TWO_LARGE_CLASSES, getTwoLargeClassesAppPath());
+ verifyMainDexContains(TWO_LARGE_CLASSES, getTwoLargeClassesAppPath(), false);
}
@Test
@@ -184,7 +160,28 @@
mainDexBuilder.add(clazz);
}
}
- verifyMainDexContains(mainDexBuilder.build(), getManyClassesAppPath());
+ verifyMainDexContains(mainDexBuilder.build(), getManyClassesSingleDexAppPath(), true);
+ verifyMainDexContains(mainDexBuilder.build(), getManyClassesMultiDexAppPath(), false);
+ }
+
+ @Test
+ public void singleClassInMainDex() throws Throwable {
+ ImmutableList<String> mainDex = ImmutableList.of(MANY_CLASSES.get(0));
+ verifyMainDexContains(mainDex, getManyClassesSingleDexAppPath(), true);
+ verifyMainDexContains(mainDex, getManyClassesMultiDexAppPath(), false);
+ }
+
+ @Test
+ public void allClassesInMainDex() throws Throwable {
+ // Degenerated case with an app thats fit into a single dex, and where the main dex list
+ // contains all classes.
+ verifyMainDexContains(MANY_CLASSES, getManyClassesSingleDexAppPath(), true);
+ }
+
+ @Test
+ public void cannotFitAllIntoMainDex() throws Throwable {
+ thrown.expect(CompilationError.class);
+ verifyMainDexContains(MANY_CLASSES, getManyClassesMultiDexAppPath(), false);
}
@Test
@@ -320,10 +317,39 @@
}
private static String typeToEntry(String type) {
- return type.replace(".", "/") + CLASS_EXTENSION;
+ return type.replace(".", "/") + FileUtils.CLASS_EXTENSION;
}
- private void verifyMainDexContains(List<String> mainDex, Path app)
+ private void failedToFindClassInExpectedFile(Path outDir, String clazz) throws IOException {
+ Files.list(outDir)
+ .filter(FileUtils::isDexFile)
+ .forEach(
+ p -> {
+ try {
+ DexInspector i = new DexInspector(AndroidApp.fromProgramFiles(p));
+ assertFalse("Found " + clazz + " in file " + p, i.clazz(clazz).isPresent());
+ } catch (IOException | ExecutionException e) {
+ e.printStackTrace();
+ }
+ });
+ fail("Failed to find class " + clazz + "in any file...");
+ }
+
+ private void assertMainDexClass(FoundClassSubject clazz, List<String> mainDex) {
+ if (!mainDex.contains(clazz.toString())) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < mainDex.size(); i++) {
+ builder.append(i == 0 ? "[" : ", ");
+ builder.append(mainDex.get(i));
+ }
+ builder.append("]");
+ fail("Class " + clazz + " found in main dex, " +
+ "only expected explicit main dex classes " + builder +" in main dex file");
+ }
+ }
+
+ private void doVerifyMainDexContains(
+ List<String> mainDex, Path app, boolean singleDexApp, boolean minimalMainDex)
throws IOException, CompilationException, ExecutionException, ProguardRuleParserException {
AndroidApp originalApp = AndroidApp.fromProgramFiles(app);
DexInspector originalInspector = new DexInspector(originalApp);
@@ -331,44 +357,42 @@
assertTrue("Class " + clazz + " does not exist in input",
originalInspector.clazz(clazz).isPresent());
}
- Path outDir = temp.newFolder("out").toPath();
- Path mainDexList = temp.getRoot().toPath().resolve("main-dex-list.txt");
+ Path outDir = temp.newFolder().toPath();
+ Path mainDexList = temp.newFile().toPath();
FileUtils.writeTextFile(mainDexList, ListUtils.map(mainDex, MainDexListTests::typeToEntry));
- Path packageDistribution = temp.getRoot().toPath().resolve("package.map");
- FileUtils.writeTextFile(packageDistribution, ImmutableList.of("a.*:2", "b.*:1"));
R8Command command =
R8Command.builder()
.addProgramFiles(app)
- .setPackageDistributionFile(packageDistribution)
.setMainDexListFile(mainDexList)
+ .setMinimalMainDex(minimalMainDex)
.setOutputPath(outDir)
.setTreeShaking(false)
.setMinification(false)
.build();
ToolHelper.runR8(command);
- assertTrue("Output run only produced one dex file. Invalid test",
- 1 < Files.list(outDir).filter(FileUtils::isDexFile).count());
+ if (!singleDexApp && !minimalMainDex) {
+ assertTrue("Output run only produced one dex file.",
+ 1 < Files.list(outDir).filter(FileUtils::isDexFile).count());
+ }
DexInspector inspector =
new DexInspector(AndroidApp.fromProgramFiles(outDir.resolve("classes.dex")));
for (String clazz : mainDex) {
if (!inspector.clazz(clazz).isPresent()) {
- Files.list(outDir)
- .filter(FileUtils::isDexFile)
- .forEach(
- p -> {
- try {
- DexInspector i = new DexInspector(AndroidApp.fromProgramFiles(p));
- assertFalse("Found " + clazz + " in file " + p, i.clazz(clazz).isPresent());
- } catch (IOException | ExecutionException e) {
- e.printStackTrace();
- }
- });
- fail("Failed to find class " + clazz + "in any file...");
+ failedToFindClassInExpectedFile(outDir, clazz);
}
}
+ if (minimalMainDex) {
+ inspector.forAllClasses(clazz -> assertMainDexClass(clazz, mainDex));
+ }
}
- private static AndroidApp generateApplication(List<String> classes, int methodCount)
+ private void verifyMainDexContains(List<String> mainDex, Path app, boolean singleDexApp)
+ throws Throwable {
+ doVerifyMainDexContains(mainDex, app, singleDexApp, false);
+ doVerifyMainDexContains(mainDex, app, singleDexApp, true);
+ }
+
+ public static AndroidApp generateApplication(List<String> classes, int minApi, int methodCount)
throws IOException, ExecutionException {
Timing timing = new Timing("MainDexListTests");
InternalOptions options = new InternalOptions();
diff --git a/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip b/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip
deleted file mode 100644
index fd4ecdc..0000000
--- a/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip
+++ /dev/null
Binary files differ
diff --git a/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip b/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip
deleted file mode 100644
index 9ad745f..0000000
--- a/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip
+++ /dev/null
Binary files differ