1// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
2// for details. All rights reserved. Use of this source code is governed by a
3// BSD-style license that can be found in the LICENSE file.
4package com.android.tools.r8;
5
6import static org.junit.Assert.assertEquals;
7import static org.junit.Assert.assertTrue;
8import static org.junit.Assert.fail;
9
10import com.android.tools.r8.dex.ApplicationReader;
11import com.android.tools.r8.dex.Constants;
12import com.android.tools.r8.graph.DexApplication;
13import com.android.tools.r8.shaking.ProguardRuleParserException;
14import com.android.tools.r8.utils.AndroidApp;
15import com.android.tools.r8.utils.InternalOptions;
16import com.android.tools.r8.utils.ListUtils;
17import com.android.tools.r8.utils.Timing;
18import com.google.common.collect.ImmutableList;
19import com.google.common.collect.ImmutableMap;
20import com.google.common.collect.ImmutableSet;
21import com.google.common.collect.Lists;
22import com.google.common.io.ByteStreams;
23import com.google.common.io.CharStreams;
24import java.io.IOException;
25import java.io.InputStream;
26import java.io.InputStreamReader;
27import java.nio.charset.StandardCharsets;
28import java.nio.file.Files;
29import java.nio.file.Path;
30import java.nio.file.Paths;
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.Collection;
34import java.util.Collections;
35import java.util.LinkedHashMap;
36import java.util.List;
37import java.util.Map;
38import java.util.Set;
39import java.util.concurrent.ExecutionException;
40import java.util.function.Consumer;
41import java.util.stream.Collectors;
42import joptsimple.internal.Strings;
43import org.junit.Assume;
44import org.junit.rules.TemporaryFolder;
45
46public class ToolHelper {
47
48  public static final String BUILD_DIR = "build/";
49  public static final String EXAMPLES_DIR = "src/test/examples/";
50  public static final String EXAMPLES_ANDROID_O_DIR = "src/test/examplesAndroidO/";
51  public static final String EXAMPLES_BUILD_DIR = BUILD_DIR + "test/examples/";
52  public static final String EXAMPLES_ANDROID_N_BUILD_DIR = BUILD_DIR + "test/examplesAndroidN/";
53  public static final String EXAMPLES_ANDROID_O_BUILD_DIR = BUILD_DIR + "test/examplesAndroidO/";
54  public static final String SMALI_BUILD_DIR = BUILD_DIR + "test/smali/";
55
56  public static final String LINE_SEPARATOR = System.getProperty("line.separator");
57
58  private static final String ANDROID_JAR_PATTERN = "third_party/android_jar/lib-v%d/android.jar";
59  private static final int DEFAULT_MIN_SDK = 14;
60
61  public enum DexVm {
62    ART_4_4_4("4.4.4"),
63    ART_5_1_1("5.1.1"),
64    ART_6_0_1("6.0.1"),
65    ART_7_0_0("7.0.0"),
66    ART_DEFAULT("default");
67
68    private static final ImmutableMap<String, DexVm> SHORT_NAME_MAP =
69        new ImmutableMap.Builder<String, DexVm>()
70            .putAll(
71                Arrays.stream(DexVm.values()).collect(
72                    Collectors.toMap(a -> a.toString(), a -> a)))
73            .build();
74
75    public String toString() {
76      return shortName;
77    }
78
79    public static DexVm fromShortName(String shortName) {
80      return SHORT_NAME_MAP.get(shortName);
81    }
82
83    public boolean isNewerThan(DexVm other) {
84      return compareTo(other) > 0;
85    }
86
87    private DexVm(String shortName) {
88      this.shortName = shortName;
89    }
90
91    private final String shortName;
92  }
93
94
95  public abstract static class CommandBuilder {
96
97    private List<String> options = new ArrayList<>();
98    private Map<String, String> systemProperties = new LinkedHashMap<>();
99    private List<String> classpaths = new ArrayList<>();
100    private String mainClass;
101    private List<String> programArguments = new ArrayList<>();
102    private List<String> bootClassPaths = new ArrayList<>();
103
104    public CommandBuilder appendArtOption(String option) {
105      options.add(option);
106      return this;
107    }
108
109    public CommandBuilder appendArtSystemProperty(String key, String value) {
110      systemProperties.put(key, value);
111      return this;
112    }
113
114    public CommandBuilder appendClasspath(String classpath) {
115      classpaths.add(classpath);
116      return this;
117    }
118
119    public CommandBuilder setMainClass(String className) {
120      this.mainClass = className;
121      return this;
122    }
123
124    public CommandBuilder appendProgramArgument(String option) {
125      programArguments.add(option);
126      return this;
127    }
128
129    public CommandBuilder appendBootClassPath(String lib) {
130      bootClassPaths.add(lib);
131      return this;
132    }
133
134    private List<String> command() {
135      List<String> result = new ArrayList<>();
136      // The art script _must_ be run with bash, bots default to /bin/dash for /bin/sh, so
137      // explicitly set it;
138      if (isLinux()) {
139        result.add("/bin/bash");
140      } else {
141        result.add("tools/docker/run.sh");
142      }
143      result.add(getExecutable());
144      for (String option : options) {
145        result.add(option);
146      }
147      for (Map.Entry<String, String> entry : systemProperties.entrySet()) {
148        StringBuilder builder = new StringBuilder("-D");
149        builder.append(entry.getKey());
150        builder.append("=");
151        builder.append(entry.getValue());
152        result.add(builder.toString());
153      }
154      if (!classpaths.isEmpty()) {
155        result.add("-cp");
156        result.add(Strings.join(classpaths, ":"));
157      }
158      if (!bootClassPaths.isEmpty()) {
159        result.add("-Xbootclasspath:" + String.join(":", bootClassPaths));
160      }
161      if (mainClass != null) {
162        result.add(mainClass);
163      }
164      for (String argument : programArguments) {
165        result.add(argument);
166      }
167      return result;
168    }
169
170    public ProcessBuilder asProcessBuilder() {
171      return new ProcessBuilder(command());
172    }
173
174    public String build() {
175      return String.join(" ", command());
176    }
177
178    protected abstract String getExecutable();
179  }
180
181  public static class ArtCommandBuilder extends CommandBuilder {
182
183    private DexVm version;
184
185    public ArtCommandBuilder() {
186    }
187
188    public ArtCommandBuilder(DexVm version) {
189      assert ART_BINARY_VERSIONS.containsKey(version);
190      this.version = version;
191    }
192
193    @Override
194    protected String getExecutable() {
195      return version != null ? getArtBinary(version) : getArtBinary();
196    }
197  }
198
199  public static class DXCommandBuilder extends CommandBuilder {
200
201    public DXCommandBuilder() {
202      appendProgramArgument("--dex");
203    }
204
205    @Override
206    protected String getExecutable() {
207      return DX;
208    }
209  }
210
211  private static class StreamReader implements Runnable {
212
213    private InputStream stream;
214    private String result;
215
216    public StreamReader(InputStream stream) {
217      this.stream = stream;
218    }
219
220    public String getResult() {
221      return result;
222    }
223
224    @Override
225    public void run() {
226      try {
227        result = CharStreams.toString(new InputStreamReader(stream, StandardCharsets.UTF_8));
228        stream.close();
229      } catch (IOException e) {
230        result = "Failed reading result for stream " + stream;
231      }
232    }
233  }
234
235  private static final String TOOLS = "tools";
236  private static final Map<DexVm, String> ART_DIRS =
237      ImmutableMap.of(
238          DexVm.ART_DEFAULT, "art",
239          DexVm.ART_7_0_0, "art-7.0.0",
240          DexVm.ART_6_0_1, "art-6.0.1",
241          DexVm.ART_5_1_1, "art-5.1.1",
242          DexVm.ART_4_4_4, "dalvik");
243  private static final Map<DexVm, String> ART_BINARY_VERSIONS =
244      ImmutableMap.of(
245          DexVm.ART_DEFAULT, "bin/art",
246          DexVm.ART_7_0_0, "bin/art",
247          DexVm.ART_6_0_1, "bin/art",
248          DexVm.ART_5_1_1, "bin/art",
249          DexVm.ART_4_4_4, "bin/dalvik");
250
251  private static final Map<DexVm, String> ART_BINARY_VERSIONS_X64 =
252      ImmutableMap.of(
253          DexVm.ART_DEFAULT, "bin/art",
254          DexVm.ART_7_0_0, "bin/art",
255          DexVm.ART_6_0_1, "bin/art");
256  private static final List<String> ART_BOOT_LIBS =
257      ImmutableList.of(
258          "core-libart-hostdex.jar",
259          "core-oj-hostdex.jar",
260          "apache-xml-hostdex.jar"
261      );
262
263  private static final String LIB_PATH = TOOLS + "/linux/art/lib";
264  private static final String DX = TOOLS + "/linux/dx/bin/dx";
265  private static final String DEX2OAT = TOOLS + "/linux/art/bin/dex2oat";
266  private static final String ANGLER_DIR = TOOLS + "/linux/art/product/angler";
267  private static final String ANGLER_BOOT_IMAGE = ANGLER_DIR + "/system/framework/boot.art";
268
269  public static String getArtDir(DexVm version) {
270    String dir = ART_DIRS.get(version);
271    if (dir == null) {
272      throw new IllegalStateException("Does not support dex vm: " + version);
273    }
274    if (isLinux() || isMac()) {
275      // The Linux version is used on Mac, where it is run in a Docker container.
276      return TOOLS + "/linux/" + dir;
277    }
278    fail("Unsupported platform, we currently only support mac and linux: " + getPlatform());
279    return ""; //never here
280  }
281
282  public static String getArtBinary(DexVm version) {
283    String binary = ART_BINARY_VERSIONS.get(version);
284    if (binary == null) {
285      throw new IllegalStateException("Does not support running with dex vm: " + version);
286    }
287    return getArtDir(version) + "/" + binary;
288  }
289
290  public static String getDefaultAndroidJar() {
291    return getAndroidJar(Constants.DEFAULT_ANDROID_API);
292  }
293
294  public static String getAndroidJar(int minSdkVersion) {
295    return String.format(
296        ANDROID_JAR_PATTERN,
297        minSdkVersion == Constants.DEFAULT_ANDROID_API ? DEFAULT_MIN_SDK : minSdkVersion);
298  }
299
300  public static Path getJdwpTestsJarPath(int minSdk) {
301    if (minSdk >= Constants.ANDROID_N_API) {
302      return Paths.get("third_party", "jdwp-tests", "apache-harmony-jdwp-tests-host.jar");
303    } else {
304      return Paths.get(ToolHelper.BUILD_DIR, "libs", "jdwp-tests-preN.jar");
305    }
306  }
307
308  // For non-Linux platforms create the temporary directory in the repository root to simplify
309  // running Art in a docker container
310  public static TemporaryFolder getTemporaryFolderForTest() {
311    return new TemporaryFolder(ToolHelper.isLinux() ? null : Paths.get("build", "tmp").toFile());
312  }
313
314  public static String getArtBinary() {
315    return getArtBinary(getDexVm());
316  }
317
318  public static Set<DexVm> getArtVersions() {
319    String artVersion = System.getProperty("dex_vm");
320    if (artVersion != null) {
321      DexVm artVersionEnum = getDexVm();
322      if (!ART_BINARY_VERSIONS.containsKey(artVersionEnum)) {
323        throw new RuntimeException("Unsupported Art version " + artVersion);
324      }
325      return ImmutableSet.of(artVersionEnum);
326    } else {
327      if (isLinux()) {
328        return ART_BINARY_VERSIONS.keySet();
329      } else {
330        return ART_BINARY_VERSIONS_X64.keySet();
331      }
332    }
333  }
334
335  public static List<String> getArtBootLibs() {
336    String prefix = getArtDir(getDexVm()) + "/";
337    List<String> result = new ArrayList<>();
338    ART_BOOT_LIBS.stream().forEach(x -> result.add(prefix + "framework/" + x));
339    return result;
340  }
341
342  // Returns if the passed in vm to use is the default.
343  public static boolean isDefaultDexVm() {
344    return getDexVm() == DexVm.ART_DEFAULT;
345  }
346
347  public static DexVm getDexVm() {
348    String artVersion = System.getProperty("dex_vm");
349    if (artVersion == null) {
350      return DexVm.ART_DEFAULT;
351    } else {
352      DexVm artVersionEnum = DexVm.fromShortName(artVersion);
353      if (artVersionEnum == null) {
354        throw new RuntimeException("Unsupported Art version " + artVersion);
355      } else {
356        return artVersionEnum;
357      }
358    }
359  }
360
361  public static int getMinApiLevelForDexVm(DexVm dexVm) {
362    switch (dexVm) {
363      case ART_DEFAULT:
364        return Constants.ANDROID_O_API;
365      case ART_7_0_0:
366        return Constants.ANDROID_N_API;
367      default:
368        return Constants.DEFAULT_ANDROID_API;
369    }
370  }
371
372  private static String getPlatform() {
373    return System.getProperty("os.name");
374  }
375
376  public static boolean isLinux() {
377    return getPlatform().startsWith("Linux");
378  }
379
380  public static boolean isMac() {
381    return getPlatform().startsWith("Mac");
382  }
383
384  public static boolean isWindows() {
385    return getPlatform().startsWith("Windows");
386  }
387
388  public static boolean artSupported() {
389    if (!isLinux() && !isMac()) {
390      System.err.println("Testing on your platform is not fully supported. " +
391          "Art does not work on on your platform.");
392      return false;
393    }
394    return true;
395  }
396
397  public static Path getClassPathForTests() {
398    return Paths.get(BUILD_DIR, "classes", "test");
399  }
400
401  public static Path getClassFileForTestClass(Class clazz) {
402    List<String> parts = Lists.newArrayList(clazz.getCanonicalName().split("\\."));
403    Class enclosing = clazz;
404    while (enclosing.getEnclosingClass() != null) {
405      parts.set(parts.size() - 2, parts.get(parts.size() - 2) + "$" + parts.get(parts.size() - 1));
406      parts.remove(parts.size() - 1);
407      enclosing = clazz.getEnclosingClass();
408    }
409    parts.set(parts.size() - 1, parts.get(parts.size() - 1) + ".class");
410    return getClassPathForTests().resolve(
411        Paths.get("", parts.toArray(new String[parts.size() - 1])));
412  }
413
414  public static DexApplication buildApplication(List<String> fileNames)
415      throws IOException, ExecutionException {
416    return new ApplicationReader(
417            AndroidApp.fromProgramFiles(ListUtils.map(fileNames, Paths::get)),
418            new InternalOptions(),
419            new Timing("ToolHelper buildApplication"))
420        .read();
421  }
422
423  public static R8Command.Builder prepareR8CommandBuilder(AndroidApp app) {
424    return R8Command.builder(app);
425  }
426
427  public static AndroidApp runR8(AndroidApp app)
428      throws ExecutionException, IOException, ProguardRuleParserException, CompilationException {
429    return runR8(R8Command.builder(app).build());
430  }
431
432  public static AndroidApp runR8(AndroidApp app, Path output)
433      throws ExecutionException, IOException, ProguardRuleParserException, CompilationException {
434    assert output != null;
435    return runR8(R8Command.builder(app).setOutputPath(output).build());
436  }
437
438  public static AndroidApp runR8(AndroidApp app, Consumer<InternalOptions> optionsConsumer)
439      throws ProguardRuleParserException, ExecutionException, IOException, CompilationException {
440    return runR8(R8Command.builder(app).build(), optionsConsumer);
441  }
442
443  public static AndroidApp runR8(R8Command command)
444      throws ProguardRuleParserException, ExecutionException, IOException {
445    return runR8(command, null);
446  }
447
448  public static AndroidApp runR8(R8Command command, Consumer<InternalOptions> optionsConsumer)
449      throws ProguardRuleParserException, ExecutionException, IOException {
450    return runR8WithFullResult(command, optionsConsumer).androidApp;
451  }
452
453  public static CompilationResult runR8WithFullResult(
454        R8Command command, Consumer<InternalOptions> optionsConsumer)
455        throws ProguardRuleParserException, ExecutionException, IOException {
456   // TODO(zerny): Should we really be adding the android library in ToolHelper?
457    AndroidApp app = command.getInputApp();
458    if (app.getClassLibraryResources().isEmpty()) {
459      app =
460          AndroidApp.builder(app)
461              .addLibraryFiles(Paths.get(getAndroidJar(command.getMinApiLevel())))
462              .build();
463    }
464    InternalOptions options = command.getInternalOptions();
465    // TODO(zerny): Should we really be setting this in ToolHelper?
466    options.ignoreMissingClasses = true;
467    if (optionsConsumer != null) {
468      optionsConsumer.accept(options);
469    }
470    CompilationResult result = R8.runForTesting(app, options);
471    R8.writeOutputs(command, options, result.androidApp);
472    return result;
473  }
474
475  public static AndroidApp runR8(String fileName, String out)
476      throws IOException, ProguardRuleParserException, ExecutionException, CompilationException {
477    return runR8(Collections.singletonList(fileName), out);
478  }
479
480  public static AndroidApp runR8(Collection<String> fileNames, String out)
481      throws IOException, ProguardRuleParserException, ExecutionException, CompilationException {
482    return R8.run(
483        R8Command.builder()
484            .addProgramFiles(ListUtils.map(fileNames, Paths::get))
485            .setOutputPath(Paths.get(out))
486            .setIgnoreMissingClasses(true)
487            .build());
488  }
489
490  public static AndroidApp runD8(AndroidApp app) throws CompilationException, IOException {
491    return runD8(app, null);
492  }
493
494  public static AndroidApp runD8(AndroidApp app, Consumer<InternalOptions> optionsConsumer)
495      throws CompilationException, IOException {
496    return runD8(D8Command.builder(app).build(), optionsConsumer);
497  }
498
499  public static AndroidApp runD8(D8Command command) throws IOException {
500    return runD8(command, null);
501  }
502
503  public static AndroidApp runD8(D8Command command, Consumer<InternalOptions> optionsConsumer)
504      throws IOException {
505    InternalOptions options = command.getInternalOptions();
506    if (optionsConsumer != null) {
507      optionsConsumer.accept(options);
508    }
509    AndroidApp result = D8.runForTesting(command.getInputApp(), options).androidApp;
510    if (command.getOutputPath() != null) {
511      result.write(command.getOutputPath(), command.getOutputMode());
512    }
513    return result;
514  }
515
516  public static AndroidApp runDexer(String fileName, String outDir, String... extraArgs)
517      throws IOException {
518    List<String> args = new ArrayList<>();
519    Collections.addAll(args, extraArgs);
520    Collections.addAll(args, "--output=" + outDir + "/classes.dex", fileName);
521    int result = runDX(args.toArray(new String[args.size()])).exitCode;
522    return result != 0 ? null : AndroidApp.fromProgramDirectory(Paths.get(outDir));
523  }
524
525  public static ProcessResult runDX(String[] args) throws IOException {
526    Assume.assumeTrue(ToolHelper.artSupported());
527    DXCommandBuilder builder = new DXCommandBuilder();
528    for (String arg : args) {
529      builder.appendProgramArgument(arg);
530    }
531    return runProcess(builder.asProcessBuilder());
532  }
533
534  public static ProcessResult runJava(Class clazz) throws Exception {
535    String main = clazz.getCanonicalName();
536    Path path = getClassPathForTests();
537    return runJava(ImmutableList.of(path.toString()), main);
538  }
539
540  public static ProcessResult runJava(List<String> classpath, String mainClass) throws IOException {
541    ProcessBuilder builder = new ProcessBuilder(
542        getJavaExecutable(), "-cp", String.join(":", classpath), mainClass);
543    return runProcess(builder);
544  }
545
546  public static ProcessResult forkD8(Path dir, String... args)
547      throws IOException, InterruptedException {
548    return forkJava(dir, D8.class, args);
549  }
550
551  public static ProcessResult forkR8(Path dir, String... args)
552      throws IOException, InterruptedException {
553    return forkJava(dir, R8.class, ImmutableList.builder()
554        .addAll(Arrays.asList(args))
555        .add("--ignore-missing-classes")
556        .build()
557        .toArray(new String[0]));
558  }
559
560  private static ProcessResult forkJava(Path dir, Class clazz, String... args)
561      throws IOException, InterruptedException {
562    List<String> command = new ImmutableList.Builder<String>()
563        .add(getJavaExecutable())
564        .add("-cp").add(System.getProperty("java.class.path"))
565        .add(clazz.getCanonicalName())
566        .addAll(Arrays.asList(args))
567        .build();
568    return runProcess(new ProcessBuilder(command).directory(dir.toFile()));
569  }
570
571  public static String getJavaExecutable() {
572    return Paths.get(System.getProperty("java.home"), "bin", "java").toString();
573  }
574
575  public static String runArtNoVerificationErrors(String file, String mainClass)
576      throws IOException {
577    return runArtNoVerificationErrors(Collections.singletonList(file), mainClass, null);
578  }
579
580  public static String runArtNoVerificationErrors(List<String> files, String mainClass,
581      Consumer<ArtCommandBuilder> extras)
582      throws IOException {
583    return runArtNoVerificationErrors(files, mainClass, extras, null);
584  }
585
586  public static String runArtNoVerificationErrors(List<String> files, String mainClass,
587      Consumer<ArtCommandBuilder> extras,
588      DexVm version)
589      throws IOException {
590    ArtCommandBuilder builder =
591        version != null ? new ArtCommandBuilder(version) : new ArtCommandBuilder();
592    files.forEach(builder::appendClasspath);
593    builder.setMainClass(mainClass);
594    if (extras != null) {
595      extras.accept(builder);
596    }
597    return runArtNoVerificationErrors(builder);
598  }
599
600  public static String runArtNoVerificationErrors(ArtCommandBuilder builder) throws IOException {
601    ProcessResult result = runArtProcess(builder);
602    if (result.stderr.contains("Verification error")) {
603      fail("Verification error: \n" + result.stderr);
604    }
605    return result.stdout;
606  }
607
608  private static ProcessResult runArtProcess(ArtCommandBuilder builder) throws IOException {
609    Assume.assumeTrue(ToolHelper.artSupported());
610    ProcessResult result = runProcess(builder.asProcessBuilder());
611    if (result.exitCode != 0) {
612      fail("Unexpected art failure: '" + result.stderr + "'\n" + result.stdout);
613    }
614    return result;
615  }
616
617  public static String runArt(ArtCommandBuilder builder) throws IOException {
618    ProcessResult result = runArtProcess(builder);
619    return result.stdout;
620  }
621
622  public static String checkArtOutputIdentical(String file1, String file2, String mainClass,
623      DexVm version)
624      throws IOException {
625    return checkArtOutputIdentical(Collections.singletonList(file1),
626        Collections.singletonList(file2), mainClass, null, version);
627  }
628
629  public static String checkArtOutputIdentical(List<String> files1, List<String> files2,
630      String mainClass,
631      Consumer<ArtCommandBuilder> extras,
632      DexVm version)
633      throws IOException {
634    // Run art on original.
635    for (String file : files1) {
636      assertTrue("file1 " + file + " must exists", Files.exists(Paths.get(file)));
637    }
638    String output1 = ToolHelper.runArtNoVerificationErrors(files1, mainClass, extras, version);
639    // Run art on R8 processed version.
640    for (String file : files2) {
641      assertTrue("file2 " + file + " must exists", Files.exists(Paths.get(file)));
642    }
643    String output2 = ToolHelper.runArtNoVerificationErrors(files2, mainClass, extras, version);
644    assertEquals(output1, output2);
645    return output1;
646  }
647
648  public static void runDex2Oat(Path file, Path outFile) throws IOException {
649    Assume.assumeTrue(ToolHelper.artSupported());
650    assert Files.exists(file);
651    assert ByteStreams.toByteArray(Files.newInputStream(file)).length > 0;
652    List<String> command = new ArrayList<>();
653    command.add(DEX2OAT);
654    command.add("--android-root=" + ANGLER_DIR);
655    command.add("--runtime-arg");
656    command.add("-Xnorelocate");
657    command.add("--boot-image=" + ANGLER_BOOT_IMAGE);
658    command.add("--dex-file=" + file.toAbsolutePath());
659    command.add("--oat-file=" + outFile.toAbsolutePath());
660    command.add("--instruction-set=arm64");
661    command.add("--compiler-filter=interpret-only");
662    ProcessBuilder builder = new ProcessBuilder(command);
663    builder.environment().put("LD_LIBRARY_PATH", LIB_PATH);
664    ProcessResult result = runProcess(builder);
665    if (result.exitCode != 0) {
666      fail("dex2oat failed, exit code " + result.exitCode + ", stderr:\n" + result.stderr);
667    }
668    if (result.stderr.contains("Verification error")) {
669      fail("Verification error: \n" + result.stderr);
670    }
671  }
672
673  public static class ProcessResult {
674
675    public final int exitCode;
676    public final String stdout;
677    public final String stderr;
678
679    ProcessResult(int exitCode, String stdout, String stderr) {
680      this.exitCode = exitCode;
681      this.stdout = stdout;
682      this.stderr = stderr;
683    }
684
685    @Override
686    public String toString() {
687      StringBuilder builder = new StringBuilder();
688      builder.append("EXIT CODE: ");
689      builder.append(exitCode);
690      builder.append("\n");
691      builder.append("STDOUT: ");
692      builder.append("\n");
693      builder.append(stdout);
694      builder.append("\n");
695      builder.append("STDERR: ");
696      builder.append("\n");
697      builder.append(stderr);
698      builder.append("\n");
699      return builder.toString();
700    }
701  }
702
703  public static ProcessResult runProcess(ProcessBuilder builder) throws IOException {
704    System.out.println(String.join(" ", builder.command()));
705    Process p = builder.start();
706    // Drain stdout and stderr so that the process does not block. Read stdout and stderr
707    // in parallel to make sure that neither buffer can get filled up which will cause the
708    // C program to block in a call to write.
709    StreamReader stdoutReader = new StreamReader(p.getInputStream());
710    StreamReader stderrReader = new StreamReader(p.getErrorStream());
711    Thread stdoutThread = new Thread(stdoutReader);
712    Thread stderrThread = new Thread(stderrReader);
713    stdoutThread.start();
714    stderrThread.start();
715    try {
716      p.waitFor();
717      stdoutThread.join();
718      stderrThread.join();
719    } catch (InterruptedException e) {
720      throw new RuntimeException("Execution interrupted", e);
721    }
722    return new ProcessResult(p.exitValue(), stdoutReader.getResult(), stderrReader.getResult());
723  }
724
725  public static AndroidApp getApp(BaseCommand command) {
726    return command.getInputApp();
727  }
728}
729