blob: 0a16436a212465aa10ba46dd32de14c7daf48494 [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;
import static com.android.tools.r8.R8Command.USAGE_MESSAGE;
import com.android.tools.r8.dex.ApplicationReader;
import com.android.tools.r8.dex.ApplicationWriter;
import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.dex.Marker;
import com.android.tools.r8.dex.Marker.Tool;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.MainDexError;
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.AppInfoWithSubtyping;
import com.android.tools.r8.graph.ClassAndMemberPublicizer;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.GraphLense;
import com.android.tools.r8.ir.conversion.IRConverter;
import com.android.tools.r8.ir.optimize.EnumOrdinalMapCollector;
import com.android.tools.r8.ir.optimize.SwitchMapCollector;
import com.android.tools.r8.naming.Minifier;
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.optimize.BridgeMethodAnalysis;
import com.android.tools.r8.optimize.DebugStripper;
import com.android.tools.r8.optimize.MemberRebindingAnalysis;
import com.android.tools.r8.optimize.VisibilityBridgeRemover;
import com.android.tools.r8.shaking.AbstractMethodRemover;
import com.android.tools.r8.shaking.AnnotationRemover;
import com.android.tools.r8.shaking.DiscardedChecker;
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.ProguardTypeMatcher;
import com.android.tools.r8.shaking.ProguardTypeMatcher.MatchSpecificType;
import com.android.tools.r8.shaking.ReasonPrinter;
import com.android.tools.r8.shaking.RootSetBuilder;
import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
import com.android.tools.r8.shaking.SimpleClassMerger;
import com.android.tools.r8.shaking.TreePruner;
import com.android.tools.r8.shaking.protolite.ProtoLiteExtension;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.CfgPrinter;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.PackageDistribution;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class R8 {
private static final String VERSION = "v0.1.19";
private final Timing timing = new Timing("R8");
private final InternalOptions options;
private R8(InternalOptions options) {
this.options = options;
options.itemFactory.resetSortedIndices();
}
// Compute the marker to be placed in the main dex file.
private static Marker getMarker(InternalOptions options) {
if (options.hasMarker()) {
return options.getMarker();
}
return new Marker(Tool.R8)
.put("version", VERSION)
.put("min-api", options.minApiLevel);
}
public static AndroidApp writeApplication(
ExecutorService executorService,
DexApplication application,
AppInfo appInfo,
byte[] deadCode,
NamingLens namingLens,
byte[] proguardSeedsData,
PackageDistribution packageDistribution,
InternalOptions options)
throws ExecutionException {
try {
Marker marker = getMarker(options);
return new ApplicationWriter(
application, appInfo, options, marker, deadCode, namingLens, proguardSeedsData)
.write(packageDistribution, executorService);
} catch (IOException e) {
throw new RuntimeException("Cannot write dex application", e);
}
}
static DexApplication optimize(
DexApplication application,
AppInfoWithSubtyping appInfo,
InternalOptions options)
throws ProguardRuleParserException, ExecutionException, IOException {
return new R8(options).optimize(application, appInfo);
}
private DexApplication optimize(DexApplication application, AppInfoWithSubtyping appInfo)
throws IOException, ProguardRuleParserException, ExecutionException {
return optimize(application, appInfo, GraphLense.getIdentityLense(),
Executors.newSingleThreadExecutor());
}
private DexApplication optimize(
DexApplication application,
AppInfoWithSubtyping appInfo,
GraphLense graphLense,
ExecutorService executorService)
throws IOException, ProguardRuleParserException, ExecutionException {
final CfgPrinter printer = options.printCfg ? new CfgPrinter() : null;
timing.begin("Create IR");
try {
IRConverter converter = new IRConverter(
timing, application, appInfo, options, printer, graphLense);
application = converter.optimize(executorService);
} finally {
timing.end();
}
if (!options.skipDebugInfoOpt && (application.getProguardMap() != null)) {
try {
timing.begin("DebugStripper");
DebugStripper stripper =
new DebugStripper(application.getProguardMap(), options, appInfo.dexItemFactory);
application.classes().forEach(stripper::processClass);
} finally {
timing.end();
}
}
if (options.printCfg) {
if (options.printCfgFile == null || options.printCfgFile.isEmpty()) {
System.out.print(printer.toString());
} else {
try (OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream(options.printCfgFile),
StandardCharsets.UTF_8)) {
writer.write(printer.toString());
}
}
}
return application;
}
private Set<DexType> filterMissingClasses(Set<DexType> missingClasses,
Set<ProguardTypeMatcher> dontWarnPatterns) {
Set<DexType> result = new HashSet<>(missingClasses);
for (ProguardTypeMatcher matcher : dontWarnPatterns) {
if (matcher instanceof MatchSpecificType) {
result.remove(((MatchSpecificType) matcher).type);
} else {
result.removeIf(matcher::matches);
}
}
return result;
}
public static void disassemble(Disassemble.DisassembleCommand command)
throws IOException, ExecutionException {
Path output = command.getOutputPath();
AndroidApp app = command.getInputApp();
InternalOptions options = command.getInternalOptions();
ExecutorService executor = ThreadUtils.getExecutorService(options);
Timing timing = new Timing("disassemble");
try {
DexApplication application = new ApplicationReader(app, options, timing).read(executor);
if (options.useSmaliSyntax) {
if (output != null) {
Files.createDirectories(output);
try (PrintStream ps = new PrintStream(
Files.newOutputStream(output.resolve("classes.smali")))) {
application.smali(options, ps);
}
} else {
application.smali(options, System.out);
}
} else {
application.disassemble(output, options);
}
} finally {
executor.shutdown();
}
}
static CompilationResult runForTesting(AndroidApp app, InternalOptions options)
throws ProguardRuleParserException, IOException {
ExecutorService executor = ThreadUtils.getExecutorService(options);
try {
return runForTesting(app, options, executor);
} finally {
executor.shutdown();
}
}
private static CompilationResult runForTesting(
AndroidApp app,
InternalOptions options,
ExecutorService executor)
throws ProguardRuleParserException, IOException {
return new R8(options).run(app, executor);
}
private CompilationResult run(AndroidApp inputApp, ExecutorService executorService)
throws IOException, ProguardRuleParserException {
if (options.quiet) {
System.setOut(new PrintStream(ByteStreams.nullOutputStream()));
}
try {
if (options.minApiLevel >= Constants.ANDROID_O_API
&& !options.mainDexKeepRules.isEmpty()) {
throw new CompilationError("Automatic main dex list is not supported when compiling for"
+ " android O and later (--min-api " + Constants.ANDROID_O_API + ")");
}
DexApplication application =
new ApplicationReader(inputApp, options, timing).read(executorService);
AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
RootSet rootSet;
byte[] proguardSeedsData = null;
timing.begin("Strip unused code");
try {
Set<DexType> missingClasses = appInfo.getMissingClasses();
missingClasses = filterMissingClasses(
missingClasses, options.proguardConfiguration.getDontWarnPatterns());
if (!missingClasses.isEmpty()) {
System.err.println();
System.err.println("WARNING, some classes are missing:");
missingClasses.forEach(clazz -> {
System.err.println(" - " + clazz.toSourceString());
});
if (!options.ignoreMissingClasses) {
throw new CompilationError(
"Shrinking can't be performed because some library classes are missing.");
}
}
rootSet = new RootSetBuilder(application, appInfo, options.proguardConfiguration.getRules())
.run(executorService);
Enqueuer enqueuer = new Enqueuer(appInfo);
enqueuer.addExtension(new ProtoLiteExtension(appInfo));
appInfo = enqueuer.traceApplication(rootSet, timing);
if (options.proguardConfiguration.isPrintSeeds()) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
PrintStream out = new PrintStream(bytes);
RootSetBuilder.writeSeeds(appInfo.withLiveness().pinnedItems, out);
out.flush();
proguardSeedsData = bytes.toByteArray();
}
if (options.useTreeShaking) {
application = new TreePruner(application, appInfo.withLiveness(), options).run();
// Recompute the subtyping information.
appInfo = appInfo.withLiveness().prunedCopyFrom(application);
new AbstractMethodRemover(appInfo).run();
new AnnotationRemover(appInfo.withLiveness(), options).run();
}
} finally {
timing.end();
}
if (options.proguardConfiguration.isAccessModificationAllowed()) {
ClassAndMemberPublicizer.run(application);
// We can now remove visibility bridges. Note that we do not need to update the
// invoke-targets here, as the existing invokes will simply dispatch to the now
// visible super-method. MemberRebinding, if run, will then dispatch it correctly.
application = new VisibilityBridgeRemover(appInfo, application).run();
}
GraphLense graphLense = GraphLense.getIdentityLense();
if (appInfo.hasLiveness()) {
graphLense = new MemberRebindingAnalysis(appInfo.withLiveness(), graphLense).run();
// Class merging requires inlining.
if (!options.skipClassMerging && options.inlineAccessors) {
timing.begin("ClassMerger");
graphLense = new SimpleClassMerger(application, appInfo.withLiveness(), graphLense,
timing).run();
timing.end();
}
appInfo = appInfo.withLiveness().prunedCopyFrom(application);
appInfo = appInfo.withLiveness().rewrittenWithLense(graphLense);
// Collect switch maps and ordinals maps.
new SwitchMapCollector(appInfo.withLiveness(), options).run();
new EnumOrdinalMapCollector(appInfo.withLiveness(), options).run();
}
graphLense = new BridgeMethodAnalysis(graphLense, appInfo.withSubtyping()).run();
application = optimize(application, appInfo, graphLense, executorService);
if (!options.mainDexKeepRules.isEmpty()) {
appInfo = new AppInfoWithSubtyping(application);
Enqueuer enqueuer = new Enqueuer(appInfo);
// Lets find classes which may have code executed before secondary dex files installation.
RootSet mainDexRootSet =
new RootSetBuilder(application, appInfo, options.mainDexKeepRules).run(executorService);
Set<DexType> mainDexBaseClasses = enqueuer.traceMainDex(mainDexRootSet, timing);
// Calculate the automatic main dex list according to legacy multidex constraints.
// Add those classes to an eventual manual list of classes.
application = new DexApplication.Builder(application)
.addToMainDexList(new MainDexListBuilder(mainDexBaseClasses, application).run())
.build();
}
appInfo = new AppInfoWithSubtyping(application);
if (options.useTreeShaking || !options.skipMinification) {
timing.begin("Post optimization code stripping");
try {
Enqueuer enqueuer = new Enqueuer(appInfo);
appInfo = enqueuer.traceApplication(rootSet, timing);
if (options.useTreeShaking) {
application = new TreePruner(application, appInfo.withLiveness(), options).run();
appInfo = appInfo.withLiveness().prunedCopyFrom(application);
// Print reasons on the application after pruning, so that we reflect the actual result.
ReasonPrinter reasonPrinter = enqueuer.getReasonPrinter(rootSet.reasonAsked);
reasonPrinter.run(application);
}
} finally {
timing.end();
}
}
// Only perform discard-checking if tree-shaking is turned on.
if (options.useTreeShaking && !rootSet.checkDiscarded.isEmpty()) {
new DiscardedChecker(rootSet, application).run();
}
timing.begin("Minification");
// If we did not have keep rules, everything will be marked as keep, so no minification
// will happen. Just avoid the overhead.
NamingLens namingLens =
options.skipMinification
? NamingLens.getIdentityLens()
: new Minifier(appInfo.withLiveness(), rootSet, options).run(timing);
timing.end();
// If a method filter is present don't produce output since the application is likely partial.
if (options.hasMethodsFilter()) {
System.out.println("Finished compilation with method filter: ");
options.methodsFilter.forEach((m) -> System.out.println(" - " + m));
return null;
}
PackageDistribution packageDistribution = null;
if (inputApp.hasPackageDistribution()) {
try (Closer closer = Closer.create()) {
packageDistribution = PackageDistribution.load(inputApp.getPackageDistribution(closer));
}
}
// Generate the resulting application resources.
AndroidApp androidApp =
writeApplication(
executorService,
application,
appInfo,
application.deadCode,
namingLens,
proguardSeedsData,
packageDistribution,
options);
options.printWarnings();
return new CompilationResult(androidApp, application, appInfo);
} catch (MainDexError mainDexError) {
throw new CompilationError(mainDexError.getMessageForR8());
} catch (ExecutionException e) {
if (e.getCause() instanceof CompilationError) {
throw (CompilationError) e.getCause();
}
throw new RuntimeException(e.getMessage(), e.getCause());
} finally {
// Dump timings.
if (options.printTimes) {
timing.report();
}
}
}
/**
* Main API entry for the R8 compiler.
*
* <p>The R8 API is intentionally limited and should "do the right thing" given a command. If this
* API does not suffice please contact the R8 team.
*
* @param command R8 command.
* @return the compilation result.
*/
public static AndroidApp run(R8Command command)
throws IOException, CompilationException, ProguardRuleParserException {
ExecutorService executorService = ThreadUtils.getExecutorService(command.getInternalOptions());
try {
return run(command, executorService);
} finally {
executorService.shutdown();
}
}
static void writeOutputs(R8Command command, InternalOptions options, AndroidApp outputApp)
throws IOException {
if (command.getOutputPath() != null) {
outputApp.write(command.getOutputPath(), options.outputMode);
}
if (options.proguardConfiguration.isPrintMapping() && !options.skipMinification) {
assert outputApp.hasProguardMap();
try (Closer closer = Closer.create()) {
OutputStream mapOut = FileUtils.openPathWithDefault(
closer,
options.proguardConfiguration.getPrintMappingFile(),
System.out,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
outputApp.writeProguardMap(mapOut);
}
}
if (options.proguardConfiguration.isPrintSeeds()) {
assert outputApp.hasProguardSeeds();
try (Closer closer = Closer.create()) {
OutputStream seedsOut = FileUtils.openPathWithDefault(
closer,
options.proguardConfiguration.getSeedFile(),
System.out,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
outputApp.writeProguardSeeds(closer, seedsOut);
}
}
if (outputApp.hasMainDexListOutput()) {
try (Closer closer = Closer.create()) {
OutputStream mainDexOut =
FileUtils.openPathWithDefault(
closer,
options.printMainDexListFile,
System.out,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
outputApp.writeMainDexList(closer, mainDexOut);
}
}
if (options.proguardConfiguration.isPrintUsage() && outputApp.hasDeadCode()) {
try (Closer closer = Closer.create()) {
OutputStream deadCodeOut = FileUtils.openPathWithDefault(
closer,
options.proguardConfiguration.getPrintUsageFile(),
System.out,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
outputApp.writeDeadCode(closer, deadCodeOut);
}
}
}
/**
* Main API entry for the R8 compiler.
*
* <p>The R8 API is intentionally limited and should "do the right thing" given a command. If this
* API does not suffice please contact the R8 team.
*
* @param command R8 command.
* @param executor executor service from which to get threads for multi-threaded processing.
* @return the compilation result.
*/
public static AndroidApp run(R8Command command, ExecutorService executor)
throws IOException, CompilationException, ProguardRuleParserException {
InternalOptions options = command.getInternalOptions();
AndroidApp outputApp =
runForTesting(command.getInputApp(), options, executor).androidApp;
writeOutputs(command, options, outputApp);
return outputApp;
}
private static void run(String[] args)
throws IOException, ProguardRuleParserException, CompilationException {
R8Command.Builder builder = R8Command.parse(args);
if (builder.getOutputPath() == null) {
builder.setOutputPath(Paths.get("."));
}
R8Command command = builder.build();
if (command.isPrintHelp()) {
System.out.println(USAGE_MESSAGE);
return;
}
if (command.isPrintVersion()) {
System.out.println("R8 " + VERSION);
return;
}
run(command);
}
public static void main(String[] args) {
try {
run(args);
} catch (NoSuchFileException e) {
System.err.println("File not found: " + e.getFile());
System.exit(1);
} catch (FileAlreadyExistsException e) {
System.err.println("File already exists: " + e.getFile());
} catch (IOException e) {
System.err.println("Failed to read or write Android app: " + e.getMessage());
System.exit(1);
} catch (ProguardRuleParserException e) {
System.err.println("Failed parsing proguard keep rules: " + e.getMessage());
System.exit(1);
} catch (RuntimeException e) {
System.err.println("Compilation failed with an internal error.");
Throwable cause = e.getCause() == null ? e : e.getCause();
cause.printStackTrace();
System.exit(1);
} catch (CompilationException e) {
System.err.println("Compilation failed: " + e.getMessage());
System.err.println(USAGE_MESSAGE);
System.exit(1);
}
}
}