blob: 7406a28de1aca5c1b4da470fad8339e84ade9a53 [file] [log] [blame]
// 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.bisect;
import com.android.tools.r8.OutputMode;
import com.android.tools.r8.ProgramConsumer;
import com.android.tools.r8.StringConsumer;
import com.android.tools.r8.bisect.BisectOptions.Result;
import com.android.tools.r8.dex.ApplicationReader;
import com.android.tools.r8.dex.ApplicationWriter;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.GraphLens;
import com.android.tools.r8.graph.InitClassLens;
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.AndroidAppConsumers;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.ImmutableList;
import com.google.common.io.CharStreams;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Bisect {
private final BisectOptions options;
private final Timing timing = new Timing("bisect");
public interface Command {
Result apply(DexApplication application) throws Exception;
}
private static class StreamReader implements Runnable {
private final InputStream stream;
private String result;
public StreamReader(InputStream stream) {
this.stream = stream;
}
public String getResult() {
return result;
}
@Override
public void run() {
try {
result = CharStreams.toString(new InputStreamReader(stream, StandardCharsets.UTF_8));
stream.close();
} catch (IOException e) {
result = "Failed reading result for stream " + stream;
}
}
}
public Bisect(BisectOptions options) {
this.options = options;
}
public static DexProgramClass run(BisectState state, Command command, Path output,
ExecutorService executor)
throws Exception {
while (true) {
DexApplication app = state.bisect();
state.write();
if (app == null) {
return state.getFinalClass();
}
if (command == null) {
writeApp(app, output, executor);
System.out.println("Bisecting completed with build in " + output + "/");
System.out.println("Continue bisection by passing either --"
+ BisectOptions.RESULT_GOOD_FLAG + " or --"
+ BisectOptions.RESULT_BAD_FLAG);
return null;
}
state.setPreviousResult(command.apply(app));
}
}
public DexProgramClass run() throws Exception {
// Setup output directory (or write to a temp dir).
Path output;
if (options.output != null) {
output = options.output;
} else {
output = Files.createTempDirectory("bisect");
}
ExecutorService executor = Executors.newWorkStealingPool();
try {
InternalOptions internal = new InternalOptions();
DexApplication goodApp = readApp(options.goodBuild, internal, executor);
DexApplication badApp = readApp(options.badBuild, internal, executor);
Path stateFile =
options.stateFile != null ? options.stateFile : output.resolve("bisect.state");
// Setup initial (or saved) bisection state.
BisectState state = new BisectState(goodApp, badApp, stateFile);
if (options.stateFile != null) {
state.read();
}
// If a "previous" result is supplied on the command line, record it.
if (options.result != Result.UNKNOWN) {
state.setPreviousResult(options.result);
}
// Setup post-build command.
Command command = null;
if (options.command != null) {
command =
(application) -> {
writeApp(application, output, executor);
return runCommand(options.command, options.goodBuild, options.badBuild, output);
};
}
// Run bisection.
return run(state, command, output, executor);
} finally {
executor.shutdown();
}
}
private static Result runCommand(Path command, Path good, Path bad, Path output)
throws IOException {
List<String> args = new ArrayList<>();
args.add("/bin/bash");
args.add(command.toString());
args.addAll(ImmutableList.of(good.toString(), bad.toString(), output.toString()));
System.out.println("Running cmd: " + String.join(" ", args));
ProcessBuilder builder = new ProcessBuilder(args);
Process process = builder.start();
StreamReader stdoutReader = new StreamReader(process.getInputStream());
StreamReader stderrReader = new StreamReader(process.getErrorStream());
Thread stdoutThread = new Thread(stdoutReader);
Thread stderrThread = new Thread(stderrReader);
stdoutThread.start();
stderrThread.start();
try {
process.waitFor();
stdoutThread.join();
stderrThread.join();
} catch (InterruptedException e) {
throw new RuntimeException("Execution interrupted", e);
}
System.out.print("OUT:\n" + stdoutReader.getResult());
System.out.print("ERR:\n" + stderrReader.getResult());
int result = process.exitValue();
if (result == 0) {
return Result.GOOD;
} else if (result == 1) {
return Result.BAD;
}
System.out.println("Failed to run command " + args);
System.out.println("Exit code: " + result + " (expected 0 for good, 1 for bad)");
throw new CompilationError("Failed to run command " + args);
}
private DexApplication readApp(Path apk, InternalOptions options, ExecutorService executor)
throws IOException, ExecutionException {
AndroidApp app = AndroidApp.builder().addProgramFiles(apk).build();
return new ApplicationReader(app, options, timing).read(executor);
}
private static void writeApp(DexApplication app, Path output, ExecutorService executor)
throws IOException, ExecutionException {
InternalOptions options = app.options;
// Save the original consumers so they can be unwrapped after write.
ProgramConsumer programConsumer = options.programConsumer;
StringConsumer proguardMapConsumer = options.proguardMapConsumer;
AndroidAppConsumers compatSink = new AndroidAppConsumers(options);
ApplicationWriter writer =
new ApplicationWriter(
AppView.createForD8(AppInfo.createInitialAppInfo(app)),
null,
GraphLens.getIdentityLens(),
InitClassLens.getThrowingInstance(),
NamingLens.getIdentityLens(),
null);
writer.write(executor);
options.signalFinishedToConsumers();
compatSink.build().writeToDirectory(output, OutputMode.DexIndexed);
// Restore original consumers.
options.programConsumer = programConsumer;
options.proguardMapConsumer = proguardMapConsumer;
}
public static DexProgramClass run(BisectOptions options) throws Exception {
return new Bisect(options).run();
}
public static void main(String[] args) throws Exception {
BisectOptions options;
try {
options = BisectOptions.parse(args);
} catch (CompilationError e) {
System.err.println(e.getMessage());
BisectOptions.printHelp(System.err);
return;
}
if (options == null) {
return;
}
DexProgramClass clazz = Bisect.run(options);
if (clazz != null) {
System.out.println("Bisection found final bad class " + clazz);
}
}
}