Add support for tracing main dex classes in R8
In addition to the given main dex list, allow to handle tracing rules
for calculating the list of classes required to be kept in the main dex
regarding legacy MultiDex constraints. Main dex list and constraints
tracing are not excliusive, when both are provided their class lists are
just merged. Runtime annotations with enum values are taken into account
precisely (if classpath is complete) and Annotations without enum are
not forced into the main dex so as classes annotated by them.
In this first step support is only added to R8.
In R8, When tree shaking and legacy main dex tracing are enabled together,
the assumption is made that tree shaking is keeping every classes with
code involved in secondary dex files installation. This allows to let tree
shaking reuse the tracing made for the main dex list instead of restarting
from scratch.
This change also introduces CompilationResult as an internal result allowing
easier inspection by tests. CompilationResult is not returned by public
R8/D8 methods.
Sources of new multidex tests are modified versions of some Jack tests.
Bug: 37772111
Change-Id: I7b741ace5a990a66cd4b991197de409c795e7d95
diff --git a/src/test/examples/multidex002/fakelibrary/MultiDex.java b/src/test/examples/multidex002/fakelibrary/MultiDex.java
new file mode 100644
index 0000000..c2d4142
--- /dev/null
+++ b/src/test/examples/multidex002/fakelibrary/MultiDex.java
@@ -0,0 +1,264 @@
+// 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 multidex002.fakelibrary;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Random;
+import java.util.Set;
+import multidexfakeframeworks.Application;
+import multidexfakeframeworks.Context;
+
+/**
+ * Monkey patches {@link Context#getClassLoader() the application context class
+ * loader} in order to load classes from more than one dex file. The primary
+ * {@code classes.dex} must contain the classes necessary for calling this
+ * class methods. Secondary dex files named classes2.dex, classes3.dex... found
+ * in the application apk will be added to the classloader after first call to
+ * {@link #install(Context)}.
+ *
+ * <p/>
+ * This library provides compatibility for platforms with API level 4 through 20. This library does
+ * nothing on newer versions of the platform which provide built-in support for secondary dex files.
+ */
+public final class MultiDex {
+
+ static final String TAG = "MultiDex";
+
+ private static final String SECONDARY_FOLDER_NAME = "secondary-dexes";
+
+ private static final int MAX_SUPPORTED_SDK_VERSION = 20;
+
+ private static final int MIN_SDK_VERSION = 4;
+
+ private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
+
+ private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
+
+ private static final Set<String> installedApk = null;
+
+ private static final boolean IS_VM_MULTIDEX_CAPABLE =
+ isVMMultidexCapable(System.getProperty("java.vm.version"));
+
+ private MultiDex() {}
+
+ /**
+ * Patches the application context class loader by appending extra dex files
+ * loaded from the application apk. This method should be called in the
+ * attachBaseContext of your {@link Application}, see
+ * {@link MultiDexApplication} for more explanation and an example.
+ *
+ * @param context application context.
+ * @throws RuntimeException if an error occurred preventing the classloader
+ * extension.
+ */
+ public static void install(Context context) {
+ if (IS_VM_MULTIDEX_CAPABLE) {
+ try {
+ clearOldDexDir(context);
+ } catch (Throwable t) {
+ }
+ return;
+ }
+ }
+
+ /**
+ * Identifies if the current VM has a native support for multidex, meaning there is no need for
+ * additional installation by this library.
+ * @return true if the VM handles multidex
+ */
+ /* package visible for test */
+ static boolean isVMMultidexCapable(String versionString) {
+ boolean isMultidexCapable = false;
+ if (versionString != null) {
+ }
+ return isMultidexCapable;
+ }
+
+ private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
+ InvocationTargetException, NoSuchMethodException, IOException {
+ int version = new Random().nextInt(23);
+ if (!files.isEmpty()) {
+ if (version >= 19) {
+ V19.install(loader, files, dexDir);
+ } else if (version >= 14) {
+ V14.install(loader, files, dexDir);
+ } else {
+ V4.install(loader, files);
+ }
+ }
+ }
+
+ /**
+ * Returns whether all files in the list are valid zip files. If {@code files} is empty, then
+ * returns true.
+ */
+ private static boolean checkValidZipFiles(List<File> files) {
+ for (File file : files) {
+ }
+ return true;
+ }
+
+ /**
+ * Locates a given field anywhere in the class inheritance hierarchy.
+ *
+ * @param instance an object to search the field into.
+ * @param name field name
+ * @return a field object
+ * @throws NoSuchFieldException if the field cannot be located
+ */
+ private static Field findField(Object instance, String name) throws NoSuchFieldException {
+ for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
+ try {
+ Field field = clazz.getDeclaredField(name);
+
+
+ if (!field.isAccessible()) {
+ field.setAccessible(true);
+ }
+
+ return field;
+ } catch (NoSuchFieldException e) {
+ // ignore and search next
+ }
+ }
+
+ throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
+ }
+
+ /**
+ * Locates a given method anywhere in the class inheritance hierarchy.
+ *
+ * @param instance an object to search the method into.
+ * @param name method name
+ * @param parameterTypes method parameter types
+ * @return a method object
+ * @throws NoSuchMethodException if the method cannot be located
+ */
+ private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
+ throws NoSuchMethodException {
+ for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
+ try {
+ Method method = clazz.getDeclaredMethod(name, parameterTypes);
+
+
+ if (!method.isAccessible()) {
+ method.setAccessible(true);
+ }
+
+ return method;
+ } catch (NoSuchMethodException e) {
+ // ignore and search next
+ }
+ }
+
+ throw new NoSuchMethodException("Method " + name + " with parameters " +
+ parameterTypes + " not found in " + instance.getClass());
+ }
+
+ /**
+ * Replace the value of a field containing a non null array, by a new array containing the
+ * elements of the original array plus the elements of extraElements.
+ * @param instance the instance whose field is to be modified.
+ * @param fieldName the field to modify.
+ * @param extraElements elements to append at the end of the array.
+ */
+ private static void expandFieldArray(Object instance, String fieldName,
+ Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
+ IllegalAccessException {
+ Field jlrField = findField(instance, fieldName);
+ Object[] original = (Object[]) jlrField.get(instance);
+ Object[] combined = (Object[]) null;
+ System.arraycopy(original, 0, combined, 0, original.length);
+ System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
+ jlrField.set(instance, combined);
+ }
+
+ private static void clearOldDexDir(Context context) throws Exception {
+
+ }
+
+ /**
+ * Installer for platform versions 19.
+ */
+ private static final class V19 {
+
+ private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
+ File optimizedDirectory)
+ throws IllegalArgumentException, IllegalAccessException,
+ NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
+ /* The patched class loader is expected to be a descendant of
+ * dalvik.system.BaseDexClassLoader. We modify its
+ * dalvik.system.DexPathList pathList field to append additional DEX
+ * file entries.
+ */
+ Field pathListField = findField(loader, "pathList");
+ Object dexPathList = pathListField.get(loader);
+ }
+
+ }
+ /**
+ * Installer for platform versions 14, 15, 16, 17 and 18.
+ */
+ private static final class V14 {
+
+ private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
+ File optimizedDirectory)
+ throws IllegalArgumentException, IllegalAccessException,
+ NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
+ /* The patched class loader is expected to be a descendant of
+ * dalvik.system.BaseDexClassLoader. We modify its
+ * dalvik.system.DexPathList pathList field to append additional DEX
+ * file entries.
+ */
+ Field pathListField = findField(loader, "pathList");
+ Object dexPathList = pathListField.get(loader);
+ }
+
+ }
+
+ /**
+ * Installer for platform versions 4 to 13.
+ */
+ private static final class V4 {
+ private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
+ throws IllegalArgumentException, IllegalAccessException,
+ NoSuchFieldException, IOException {
+ /* The patched class loader is expected to be a descendant of
+ * dalvik.system.DexClassLoader. We modify its
+ * fields mPaths, mFiles, mZips and mDexs to append additional DEX
+ * file entries.
+ */
+ int extraSize = additionalClassPathEntries.size();
+
+ Field pathField = findField(loader, "path");
+
+ StringBuilder path = new StringBuilder((String) pathField.get(loader));
+ String[] extraPaths = new String[extraSize];
+ File[] extraFiles = new File[extraSize];
+ for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
+ iterator.hasNext();) {
+ File additionalEntry = iterator.next();
+ String entryPath = additionalEntry.getAbsolutePath();
+ path.append(':').append(entryPath);
+ int index = iterator.previousIndex();
+ extraPaths[index] = entryPath;
+ extraFiles[index] = additionalEntry;
+ }
+
+ pathField.set(loader, path.toString());
+ expandFieldArray(loader, "mPaths", extraPaths);
+ expandFieldArray(loader, "mFiles", extraFiles);
+ }
+ }
+
+}
diff --git a/src/test/examples/multidex002/fakelibrary/MultiDexApplication.java b/src/test/examples/multidex002/fakelibrary/MultiDexApplication.java
new file mode 100644
index 0000000..1813392
--- /dev/null
+++ b/src/test/examples/multidex002/fakelibrary/MultiDexApplication.java
@@ -0,0 +1,29 @@
+// 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 multidex002.fakelibrary;
+
+import multidexfakeframeworks.Application;
+import multidexfakeframeworks.Context;
+
+/**
+ * Minimal MultiDex capable application. To use the legacy multidex library there is 3 possibility:
+ * <ul>
+ * <li>Declare this class as the application in your AndroidManifest.xml.</li>
+ * <li>Have your {@link Application} extends this class.</li>
+ * <li>Have your {@link Application} override attachBaseContext starting with<br>
+ * <code>
+ protected void attachBaseContext(Context base) {<br>
+ super.attachBaseContext(base);<br>
+ MultiDex.install(this);
+ </code></li>
+ * <ul>
+ */
+public class MultiDexApplication extends Application {
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ MultiDex.install(this);
+ }
+}
diff --git a/src/test/examples/multidex002/fakelibrary/MultiDexExtractor.java b/src/test/examples/multidex002/fakelibrary/MultiDexExtractor.java
new file mode 100644
index 0000000..b80fcc8
--- /dev/null
+++ b/src/test/examples/multidex002/fakelibrary/MultiDexExtractor.java
@@ -0,0 +1,141 @@
+// 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 multidex002.fakelibrary;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.util.List;
+import multidexfakeframeworks.Context;
+
+/**
+ * Exposes application secondary dex files as files in the application data
+ * directory.
+ */
+final class MultiDexExtractor {
+
+ private static final String TAG = MultiDex.TAG;
+
+ /**
+ * We look for additional dex files named {@code classes2.dex},
+ * {@code classes3.dex}, etc.
+ */
+ private static final String DEX_PREFIX = "classes";
+ private static final String DEX_SUFFIX = ".dex";
+
+ private static final String EXTRACTED_NAME_EXT = ".classes";
+ private static final String EXTRACTED_SUFFIX = ".zip";
+ private static final int MAX_EXTRACT_ATTEMPTS = 3;
+
+ private static final String PREFS_FILE = "multidex.version";
+ private static final String KEY_TIME_STAMP = "timestamp";
+ private static final String KEY_CRC = "crc";
+ private static final String KEY_DEX_NUMBER = "dex.number";
+
+ /**
+ * Size of reading buffers.
+ */
+ private static final int BUFFER_SIZE = 0x4000;
+ /* Keep value away from 0 because it is a too probable time stamp value */
+ private static final long NO_VALUE = -1L;
+
+
+ private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
+ throws IOException {
+
+ final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
+ int totalDexNumber = 1;
+ final List<File> files = null;
+
+ for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
+ String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
+ File extractedFile = new File(dexDir, fileName);
+ if (extractedFile.isFile()) {
+ files.add(extractedFile);
+ } else {
+ throw new IOException("Missing extracted secondary dex file '" +
+ extractedFile.getPath() + "'");
+ }
+ }
+
+ return files;
+ }
+
+ private static long getTimeStamp(File archive) {
+ long timeStamp = archive.lastModified();
+ if (timeStamp == NO_VALUE) {
+ // never return NO_VALUE
+ timeStamp--;
+ }
+ return timeStamp;
+ }
+
+
+ private static long getZipCrc(File archive) throws IOException {
+ long computedValue = ZipUtil.getZipCrc(archive);
+ if (computedValue == NO_VALUE) {
+ // never return NO_VALUE
+ computedValue--;
+ }
+ return computedValue;
+ }
+
+ private static List<File> performExtractions(File sourceApk, File dexDir)
+ throws IOException {
+
+ final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
+
+ // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
+ // contains a secondary dex file in there is not consistent with the latest apk. Otherwise,
+ // multi-process race conditions can cause a crash loop where one process deletes the zip
+ // while another had created it.
+ prepareDexDir(dexDir, extractedFilePrefix);
+
+ List<File> files = null;
+
+ return files;
+ }
+
+ /**
+ * This removes any files that do not have the correct prefix.
+ */
+ private static void prepareDexDir(File dexDir, final String extractedFilePrefix)
+ throws IOException {
+ dexDir.mkdir();
+ if (!dexDir.isDirectory()) {
+ throw new IOException("Failed to create dex directory " + dexDir.getPath());
+ }
+
+ // Clean possible old files
+ FileFilter filter = new FileFilter() {
+
+ @Override
+ public boolean accept(File pathname) {
+ return !pathname.getName().startsWith(extractedFilePrefix);
+ }
+ };
+ File[] files = dexDir.listFiles(filter);
+ if (files == null) {
+ return;
+ }
+ for (File oldFile : files) {
+ if (!oldFile.delete()) {
+ } else {
+ }
+ }
+ }
+
+ /**
+ * Closes the given {@code Closeable}. Suppresses any IO exceptions.
+ */
+ private static void closeQuietly(Closeable closeable) {
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ }
+ }
+
+}
diff --git a/src/test/examples/multidex002/fakelibrary/ZipUtil.java b/src/test/examples/multidex002/fakelibrary/ZipUtil.java
new file mode 100644
index 0000000..1875556
--- /dev/null
+++ b/src/test/examples/multidex002/fakelibrary/ZipUtil.java
@@ -0,0 +1,21 @@
+// 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 multidex002.fakelibrary;
+
+import java.io.File;
+
+/**
+ * Stub.
+ */
+final class ZipUtil {
+ static class EmptyInner {
+ }
+
+ static long getZipCrc(File apk) {
+ return 1;
+ }
+
+
+}