blob: 5e31c47e3bc5803b1669d7dd9ab34f2f4421f319 [file] [log] [blame]
// Copyright (c) 2016, 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.dex;
import static com.android.tools.r8.graph.ClassKind.CLASSPATH;
import static com.android.tools.r8.graph.ClassKind.LIBRARY;
import static com.android.tools.r8.graph.ClassKind.PROGRAM;
import static com.android.tools.r8.utils.ExceptionUtils.unwrapExecutionException;
import com.android.tools.r8.ClassFileResourceProvider;
import com.android.tools.r8.DataResourceProvider;
import com.android.tools.r8.Diagnostic;
import com.android.tools.r8.ProgramResource;
import com.android.tools.r8.ProgramResource.Kind;
import com.android.tools.r8.ProgramResourceProvider;
import com.android.tools.r8.ResourceException;
import com.android.tools.r8.StringResource;
import com.android.tools.r8.dump.DumpOptions;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.UnsupportedMainDexListUsageDiagnostic;
import com.android.tools.r8.graph.ApplicationReaderMap;
import com.android.tools.r8.graph.ClassKind;
import com.android.tools.r8.graph.DexAnnotation;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexApplicationReadFlags;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexClasspathClass;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexLibraryClass;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.JarApplicationReader;
import com.android.tools.r8.graph.JarClassFileReader;
import com.android.tools.r8.graph.LazyLoadedDexApplication;
import com.android.tools.r8.naming.ClassNameMapper;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.shaking.MainDexInfo;
import com.android.tools.r8.threading.TaskCollection;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.ClassProvider;
import com.android.tools.r8.utils.ClasspathClassCollection;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.DexVersion;
import com.android.tools.r8.utils.DumpInputFlags;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.LibraryClassCollection;
import com.android.tools.r8.utils.MainDexListParser;
import com.android.tools.r8.utils.StringDiagnostic;
import com.android.tools.r8.utils.Timing;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
public class ApplicationReader {
private final InternalOptions options;
private final DexItemFactory itemFactory;
private final Timing timing;
private final AndroidApp inputApp;
private DexApplicationReadFlags flags;
public interface ProgramClassConflictResolver {
DexProgramClass resolveClassConflict(DexProgramClass a, DexProgramClass b);
}
public ApplicationReader(AndroidApp inputApp, InternalOptions options, Timing timing) {
this.options = options;
itemFactory = options.itemFactory;
this.timing = timing;
this.inputApp = inputApp;
}
public LazyLoadedDexApplication read() throws IOException {
return read((StringResource) null);
}
public LazyLoadedDexApplication read(StringResource proguardMap) throws IOException {
ExecutorService executor = options.getThreadingModule().createSingleThreadedExecutorService();
try {
return read(proguardMap, executor);
} finally {
executor.shutdown();
}
}
public final LazyLoadedDexApplication read(ExecutorService executorService) throws IOException {
return read(inputApp.getProguardMapInputData(), executorService);
}
public final LazyLoadedDexApplication readWithoutDumping(ExecutorService executorService)
throws IOException {
return read(inputApp.getProguardMapInputData(), executorService, DumpInputFlags.noDump());
}
public final LazyLoadedDexApplication read(
StringResource proguardMap,
ExecutorService executorService)
throws IOException {
return read(proguardMap, executorService, options.getDumpInputFlags());
}
public final LazyLoadedDexApplication read(
StringResource proguardMap,
ExecutorService executorService,
DumpInputFlags dumpInputFlags)
throws IOException {
assert verifyMainDexOptionsCompatible(inputApp, options);
dumpApplication(dumpInputFlags);
if (options.testing.verifyInputs) {
inputApp.validateInputs();
}
timing.begin("DexApplication.read");
final LazyLoadedDexApplication.Builder builder = DexApplication.builder(options, timing);
TaskCollection<?> tasks = new TaskCollection<>(options, executorService);
try {
// Still preload some of the classes, primarily for two reasons:
// (a) class lazy loading is not supported for DEX files
// now and current implementation of parallel DEX file
// loading will be lost with on-demand class loading.
// (b) some of the class file resources don't provide information
// about class descriptor.
// TODO: try and preload less classes.
readProguardMap(proguardMap, builder, tasks);
ClassReader classReader = new ClassReader(tasks);
classReader.readSources();
tasks.await();
flags = classReader.getDexApplicationReadFlags();
builder.setFlags(flags);
classReader.initializeLazyClassCollection(builder);
for (ProgramResourceProvider provider : inputApp.getProgramResourceProviders()) {
DataResourceProvider dataResourceProvider = provider.getDataResourceProvider();
if (dataResourceProvider != null) {
builder.addDataResourceProvider(dataResourceProvider);
}
}
} catch (ExecutionException e) {
throw unwrapExecutionException(e);
} catch (ResourceException e) {
throw options.reporter.fatalError(new StringDiagnostic(e.getMessage(), e.getOrigin()));
} finally {
timing.end();
}
return builder.build();
}
private void dumpApplication(DumpInputFlags dumpInputFlags) {
DumpOptions dumpOptions = options.dumpOptions;
if (dumpOptions == null || !dumpInputFlags.shouldDump(dumpOptions)) {
return;
}
Path dumpOutput = dumpInputFlags.getDumpPath();
timing.begin("ApplicationReader.dump");
inputApp.dump(dumpOutput, dumpOptions, options);
timing.end();
Diagnostic message = new StringDiagnostic("Dumped compilation inputs to: " + dumpOutput);
if (dumpInputFlags.shouldFailCompilation()) {
throw options.reporter.fatalError(message);
} else if (dumpInputFlags.shouldLogDumpInfoMessage()) {
options.reporter.info(message);
}
}
public MainDexInfo readMainDexClasses(DexApplication app) {
return readMainDexClasses(app, flags.hasReadProgramClassFromCf());
}
public MainDexInfo readMainDexClassesForR8(DexApplication app) {
// Officially R8 only support reading CF program inputs, thus we always generate a deprecated
// diagnostic if main-dex list is used.
return readMainDexClasses(app, true);
}
private MainDexInfo readMainDexClasses(DexApplication app, boolean emitDeprecatedDiagnostics) {
MainDexInfo.Builder builder = MainDexInfo.none().builder();
if (inputApp.hasMainDexList()) {
for (StringResource resource : inputApp.getMainDexListResources()) {
if (emitDeprecatedDiagnostics) {
options.reporter.error(new UnsupportedMainDexListUsageDiagnostic(resource.getOrigin()));
}
addToMainDexClasses(app, builder, MainDexListParser.parseList(resource, itemFactory));
}
if (!inputApp.getMainDexClasses().isEmpty()) {
if (emitDeprecatedDiagnostics) {
options.reporter.error(new UnsupportedMainDexListUsageDiagnostic(Origin.unknown()));
}
addToMainDexClasses(
app,
builder,
inputApp.getMainDexClasses().stream()
.map(clazz -> itemFactory.createType(DescriptorUtils.javaTypeToDescriptor(clazz)))
.collect(Collectors.toList()));
}
}
return builder.buildList();
}
private void addToMainDexClasses(
DexApplication app, MainDexInfo.Builder builder, Iterable<DexType> types) {
for (DexType type : types) {
DexProgramClass clazz = app.programDefinitionFor(type);
if (clazz != null) {
builder.addList(clazz);
} else if (!options.ignoreMainDexMissingClasses) {
options.reporter.warning(
new StringDiagnostic(
"Application does not contain `"
+ type.toSourceString()
+ "` as referenced in main-dex-list."));
}
}
}
private static boolean verifyMainDexOptionsCompatible(
AndroidApp inputApp, InternalOptions options) {
if (!options.isGeneratingDex()) {
return true;
}
// Native multidex is supported from L, but the compiler supports compiling to L/21 using
// legacy multidex as there are some devices that have issues with it still.
AndroidApiLevel nativeMultiDex = AndroidApiLevel.L_MR1;
if (options.getMinApiLevel().isLessThan(nativeMultiDex)) {
return true;
}
assert options.mainDexKeepRules.isEmpty();
assert options.mainDexListConsumer == null;
assert !inputApp.hasMainDexList();
return true;
}
private AndroidApiLevel validateOrComputeMinApiLevel(
AndroidApiLevel computedMinApiLevel, DexReader dexReader) {
DexVersion version = dexReader.getDexVersion();
if (!options.testing.dexContainerExperiment
&& version.getIntValue() == InternalOptions.EXPERIMENTAL_DEX_VERSION) {
throwIncompatibleDexVersionAndMinApi(version);
}
if (options.getMinApiLevel() == AndroidApiLevel.getDefault()) {
computedMinApiLevel = computedMinApiLevel.max(AndroidApiLevel.getMinAndroidApiLevel(version));
} else if (!version.matchesApiLevel(options.getMinApiLevel())) {
throwIncompatibleDexVersionAndMinApi(version);
}
return computedMinApiLevel;
}
private void throwIncompatibleDexVersionAndMinApi(DexVersion version) {
throw new CompilationError(
"Dex file with version '"
+ version.getIntValue()
+ "' cannot be used with min sdk level '"
+ options.getMinApiLevel()
+ "'.");
}
private void readProguardMap(
StringResource map, DexApplication.Builder<?> builder, TaskCollection<?> tasks)
throws ExecutionException {
// Read the Proguard mapping file in parallel with DexCode and DexProgramClass items.
if (map == null) {
return;
}
tasks.submit(
() -> {
try {
builder.setProguardMap(
ClassNameMapper.mapperFromString(
map.getString(),
options.reporter,
options.mappingComposeOptions().allowEmptyMappedRanges,
options.testing.enableExperimentalMapFileVersion,
true));
} catch (IOException | ResourceException e) {
throw new CompilationError("Failure to read proguard map file", e, map.getOrigin());
}
});
}
private final class ClassReader {
private final TaskCollection<?> tasks;
// We use concurrent queues to collect classes
// since the classes can be collected concurrently.
private final Queue<DexProgramClass> programClasses = new ConcurrentLinkedQueue<>();
private final Queue<DexClasspathClass> classpathClasses = new ConcurrentLinkedQueue<>();
private final Queue<DexLibraryClass> libraryClasses = new ConcurrentLinkedQueue<>();
// Jar application reader to share across all class readers.
private final DexApplicationReadFlags.Builder readFlagsBuilder =
DexApplicationReadFlags.builder();
private final JarApplicationReader application =
new JarApplicationReader(options, readFlagsBuilder);
// Flag of which input resource types have flown into the program classes.
// Note that this is just at the level of the resources having been given.
// It is possible to have, e.g., an empty dex file, so no classes, but this will still be true
// as there was a dex resource.
private boolean hasReadProgramResourceFromCf = false;
private boolean hasReadProgramResourceFromDex = false;
ClassReader(TaskCollection<?> tasks) {
this.tasks = tasks;
}
public DexApplicationReadFlags getDexApplicationReadFlags() {
return readFlagsBuilder
.setHasReadProgramClassFromDex(hasReadProgramResourceFromDex)
.setHasReadProgramClassFromCf(hasReadProgramResourceFromCf)
.build();
}
private void readDexSources(List<ProgramResource> dexSources, Queue<DexProgramClass> classes)
throws IOException, ResourceException, ExecutionException {
if (dexSources.isEmpty()) {
return;
}
hasReadProgramResourceFromDex = true;
List<DexParser<DexProgramClass>> dexParsers = new ArrayList<>(dexSources.size());
AndroidApiLevel computedMinApiLevel = options.getMinApiLevel();
for (ProgramResource input : dexSources) {
DexReader dexReader = new DexReader(input);
if (options.passthroughDexCode) {
if (!options.testing.dexContainerExperiment) {
computedMinApiLevel = validateOrComputeMinApiLevel(computedMinApiLevel, dexReader);
} else {
assert dexReader.getDexVersion() == DexVersion.V41;
}
}
if (!options.testing.dexContainerExperiment) {
if (dexReader.getDexVersion().isContainerDex()) {
throw new ResourceException(
input.getOrigin(),
"Experimental container DEX version "
+ dexReader.getDexVersion()
+ " is not supported");
}
dexParsers.add(new DexParser<>(dexReader, PROGRAM, options));
} else {
addDexParsersForContainer(dexParsers, dexReader);
}
}
options.setMinApiLevel(computedMinApiLevel);
for (DexParser<DexProgramClass> dexParser : dexParsers) {
dexParser.populateIndexTables();
}
// Read the DexCode items and DexProgramClass items in parallel.
if (!options.skipReadingDexCode) {
ApplicationReaderMap applicationReaderMap = ApplicationReaderMap.getInstance(options);
if (!options.testing.dexContainerExperiment) {
for (DexParser<DexProgramClass> dexParser : dexParsers) {
tasks.submit(
() -> {
dexParser.addClassDefsTo(
classes::add, applicationReaderMap); // Depends on Methods, Code items etc.
});
}
} else {
// All Dex parsers use the same DEX reader, so don't process in parallel.
for (int i = 0; i < dexParsers.size(); i++) {
dexParsers.get(i).addClassDefsTo(classes::add, applicationReaderMap);
}
}
}
}
private void addDexParsersForContainer(
List<DexParser<DexProgramClass>> dexParsers, DexReader dexReader) {
// Find the start offsets of each dex section.
IntList offsets = new IntArrayList();
dexReader.setByteOrder();
int offset = 0;
while (offset < dexReader.end()) {
offsets.add(offset);
DexReader tmp = new DexReader(Origin.unknown(), dexReader.buffer.array(), offset);
assert tmp.getDexVersion() == DexVersion.V41;
assert dexReader.getUint(offset + Constants.HEADER_SIZE_OFFSET)
== Constants.TYPE_HEADER_ITEM_SIZE_V41;
assert dexReader.getUint(offset + Constants.CONTAINER_OFF_OFFSET) == offset;
int dataSize = dexReader.getUint(offset + Constants.DATA_SIZE_OFFSET);
int dataOffset = dexReader.getUint(offset + Constants.DATA_OFF_OFFSET);
int file_size = dexReader.getUint(offset + Constants.FILE_SIZE_OFFSET);
assert dataOffset == 0;
assert dataSize == 0;
offset += file_size;
}
assert offset == dexReader.end();
// Create a parser for the last section with string data.
DexParser<DexProgramClass> last =
new DexParser<>(dexReader, PROGRAM, options, offsets.getInt(offsets.size() - 1), null);
// Create a parsers for the remaining sections with reference to the string data.
for (int i = 0; i < offsets.size() - 1; i++) {
dexParsers.add(new DexParser<>(dexReader, PROGRAM, options, offsets.getInt(i), last));
}
dexParsers.add(last);
}
private boolean includeAnnotationClass(DexProgramClass clazz) {
if (!options.pruneNonVissibleAnnotationClasses) {
return true;
}
DexAnnotation retentionAnnotation =
clazz.annotations().getFirstMatching(itemFactory.retentionType);
// Default is CLASS retention, read if retained.
if (retentionAnnotation == null) {
return DexAnnotation.retainCompileTimeAnnotation(clazz.getType(), application.options);
}
// Otherwise only read runtime visible annotations.
return retentionAnnotation.annotation.toString().contains("RUNTIME");
}
private void readClassSources(
List<ProgramResource> classSources, Queue<DexProgramClass> classes)
throws ExecutionException {
if (classSources.isEmpty()) {
return;
}
hasReadProgramResourceFromCf = true;
JarClassFileReader<DexProgramClass> reader =
new JarClassFileReader<>(
application,
clazz -> {
if (clazz.isAnnotation() && !includeAnnotationClass(clazz)) {
return;
}
classes.add(clazz);
},
PROGRAM);
// Read classes in parallel.
for (ProgramResource input : classSources) {
tasks.submit(() -> reader.read(input));
}
}
void readSources() throws IOException, ResourceException, ExecutionException {
Collection<ProgramResource> resources = inputApp.computeAllProgramResources();
List<ProgramResource> dexResources = new ArrayList<>(resources.size());
List<ProgramResource> cfResources = new ArrayList<>(resources.size());
for (ProgramResource resource : resources) {
if (resource.getKind() == Kind.DEX) {
dexResources.add(resource);
} else {
assert resource.getKind() == Kind.CF;
cfResources.add(resource);
}
}
readDexSources(dexResources, programClasses);
readClassSources(cfResources, programClasses);
}
private <T extends DexClass> ClassProvider<T> buildClassProvider(
ClassKind<T> classKind,
Queue<T> preloadedClasses,
List<ClassFileResourceProvider> resourceProviders,
JarApplicationReader reader) {
List<ClassProvider<T>> providers = new ArrayList<>();
// Preloaded classes.
if (!preloadedClasses.isEmpty()) {
providers.add(ClassProvider.forPreloadedClasses(classKind, preloadedClasses));
}
// Class file resource providers.
for (ClassFileResourceProvider provider : resourceProviders) {
providers.add(ClassProvider.forClassFileResources(classKind, provider, reader));
}
// Combine if needed.
if (providers.isEmpty()) {
return null;
}
return providers.size() == 1 ? providers.get(0)
: ClassProvider.combine(classKind, providers);
}
void initializeLazyClassCollection(LazyLoadedDexApplication.Builder builder) {
// Add all program classes to the builder.
for (DexProgramClass clazz : programClasses) {
builder.addProgramClass(clazz.asProgramClass());
}
// Create classpath class collection if needed.
ClassProvider<DexClasspathClass> classpathClassProvider = buildClassProvider(CLASSPATH,
classpathClasses, inputApp.getClasspathResourceProviders(), application);
if (classpathClassProvider != null) {
builder.setClasspathClassCollection(new ClasspathClassCollection(classpathClassProvider));
}
// Create library class collection if needed.
ClassProvider<DexLibraryClass> libraryClassProvider = buildClassProvider(LIBRARY,
libraryClasses, inputApp.getLibraryResourceProviders(), application);
if (libraryClassProvider != null) {
builder.setLibraryClassCollection(new LibraryClassCollection(libraryClassProvider));
}
// Transfer the keep declarations found during reading.
builder.setKeepDeclarations(application.getKeepDeclarations());
}
}
}