| // 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.getDefault(), |
| 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); |
| } |
| } |
| } |