Update Smali, Dx, and DexMerger tasks to use Gradle workers

This allows these tasks to run in parallel rather than in serial, leading to improved build times.

Before:

    BUILD SUCCESSFUL in 1m 3s
    317 actionable tasks: 192 executed, 125 up-to-date

After:

    BUILD SUCCESSFUL in 25s
    317 actionable tasks: 192 executed, 125 up-to-date

Change-Id: I9089a9b80cb1f0a570fc3135afbbbc6311fd4a2c
diff --git a/buildSrc/src/main/java/dx/DexMerger.java b/buildSrc/src/main/java/dx/DexMerger.java
deleted file mode 100644
index 6303009..0000000
--- a/buildSrc/src/main/java/dx/DexMerger.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// 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 dx;
-
-import java.io.File;
-import java.io.IOException;
-import org.gradle.api.Action;
-import org.gradle.api.DefaultTask;
-import org.gradle.api.UncheckedIOException;
-import org.gradle.api.file.FileCollection;
-import org.gradle.api.file.FileTree;
-import org.gradle.api.tasks.InputFiles;
-import org.gradle.api.tasks.OutputDirectory;
-import org.gradle.api.tasks.OutputFile;
-import org.gradle.api.tasks.TaskAction;
-import org.gradle.process.ExecSpec;
-import utils.Utils;
-
-public class DexMerger extends DefaultTask {
-
-  private FileCollection source;
-  private File destination;
-  private File dexMergerExecutable;
-  private boolean debug;
-
-  @InputFiles
-  public FileCollection getSource() {
-    return source;
-  }
-
-  public void setSource(FileCollection source) {
-    this.source = source;
-  }
-
-  @OutputFile
-  public File getDestination() {
-    return destination;
-  }
-
-  public void setDestination(File destination) {
-    this.destination = destination;
-  }
-
-  public File getDexMergerExecutable() {
-    return dexMergerExecutable;
-  }
-
-  public void setDexMergerExecutable(File dexMergerExecutable) {
-    this.dexMergerExecutable = dexMergerExecutable;
-  }
-
-  @TaskAction
-  void exec() {
-    getProject().exec(new Action<ExecSpec>() {
-      @Override
-      public void execute(ExecSpec execSpec) {
-        try {
-          if (dexMergerExecutable == null) {
-            dexMergerExecutable = Utils.dexMergerExecutable();
-          }
-          execSpec.setExecutable(dexMergerExecutable);
-          execSpec.args(destination.getCanonicalPath());
-          execSpec.args(source.getFiles());
-        } catch (IOException e) {
-          throw new UncheckedIOException(e);
-        }
-      }
-    });
-  }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/dx/DexMergerTask.java b/buildSrc/src/main/java/dx/DexMergerTask.java
new file mode 100644
index 0000000..735afa7
--- /dev/null
+++ b/buildSrc/src/main/java/dx/DexMergerTask.java
@@ -0,0 +1,110 @@
+// Copyright (c) 2019, 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 dx;
+
+import com.google.common.base.Optional;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import javax.inject.Inject;
+import org.gradle.api.Action;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.UncheckedIOException;
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.process.ExecSpec;
+import org.gradle.workers.IsolationMode;
+import org.gradle.workers.WorkerConfiguration;
+import org.gradle.workers.WorkerExecutor;
+import utils.Utils;
+
+public class DexMergerTask extends DefaultTask {
+
+  private final WorkerExecutor workerExecutor;
+
+  private FileCollection source;
+  private File destination;
+  private Optional<File> dexMergerExecutable = Optional.absent(); // Worker API cannot handle null.
+
+  @Inject
+  public DexMergerTask(WorkerExecutor workerExecutor) {
+    this.workerExecutor = workerExecutor;
+  }
+
+  @InputFiles
+  public FileCollection getSource() {
+    return source;
+  }
+
+  public void setSource(FileCollection source) {
+    this.source = source;
+  }
+
+  @OutputFile
+  public File getDestination() {
+    return destination;
+  }
+
+  public void setDestination(File destination) {
+    this.destination = destination;
+  }
+
+  @InputFile
+  @org.gradle.api.tasks.Optional
+  public File getDexMergerExecutable() {
+    return dexMergerExecutable.orNull();
+  }
+
+  public void setDexMergerExecutable(File dexMergerExecutable) {
+    this.dexMergerExecutable = Optional.fromNullable(dexMergerExecutable);
+  }
+
+  @TaskAction
+  void exec() {
+    workerExecutor.submit(RunDexMerger.class, config -> {
+      config.setIsolationMode(IsolationMode.NONE);
+      config.params(source.getFiles(), destination, dexMergerExecutable);
+    });
+  }
+
+  public static class RunDexMerger implements Runnable {
+    private final Set<File> sources;
+    private final File destination;
+    private final Optional<File> dexMergerExecutable;
+
+    @Inject
+    public RunDexMerger(Set<File> sources, File destination, Optional<File> dexMergerExecutable) {
+      this.sources = sources;
+      this.destination = destination;
+      this.dexMergerExecutable = dexMergerExecutable;
+    }
+
+    @Override
+    public void run() {
+      try {
+        List<String> command = new ArrayList<>();
+        command.add(dexMergerExecutable.or(Utils::dexMergerExecutable).getCanonicalPath());
+        command.add(destination.getCanonicalPath());
+        for (File source : sources) {
+          command.add(source.getCanonicalPath());
+        }
+
+        Process dexMerger = new ProcessBuilder(command).inheritIO().start();
+        int exitCode = dexMerger.waitFor();
+        if (exitCode != 0) {
+          throw new RuntimeException("Dex merger failed with code " + exitCode);
+        }
+      } catch (IOException e) {
+        throw new java.io.UncheckedIOException(e);
+      } catch (InterruptedException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+}
diff --git a/buildSrc/src/main/java/dx/Dx.java b/buildSrc/src/main/java/dx/Dx.java
deleted file mode 100644
index b03ad0f..0000000
--- a/buildSrc/src/main/java/dx/Dx.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// 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 dx;
-
-import java.io.File;
-import java.io.IOException;
-import org.gradle.api.Action;
-import org.gradle.api.DefaultTask;
-import org.gradle.api.UncheckedIOException;
-import org.gradle.api.file.FileCollection;
-import org.gradle.api.file.FileTree;
-import org.gradle.api.tasks.InputFiles;
-import org.gradle.api.tasks.OutputDirectory;
-import org.gradle.api.tasks.OutputFile;
-import org.gradle.api.tasks.TaskAction;
-import org.gradle.process.ExecSpec;
-import utils.Utils;
-
-public class Dx extends DefaultTask {
-
-  private FileCollection source;
-  private File destination;
-  private File dxExecutable;
-  private boolean debug;
-
-  @InputFiles
-  public FileCollection getSource() {
-    return source;
-  }
-
-  public void setSource(FileCollection source) {
-    this.source = source;
-  }
-
-  @OutputDirectory
-  public File getDestination() {
-    return destination;
-  }
-
-  public void setDestination(File destination) {
-    this.destination = destination;
-  }
-
-  public File getDxExecutable() {
-    return dxExecutable;
-  }
-
-  public void setDxExecutable(File dxExecutable) {
-    this.dxExecutable = dxExecutable;
-  }
-
-  public boolean isDebug() {
-    return debug;
-  }
-
-  public void setDebug(boolean debug) {
-    this.debug = debug;
-  }
-
-  @TaskAction
-  void exec() {
-    getProject().exec(new Action<ExecSpec>() {
-      @Override
-      public void execute(ExecSpec execSpec) {
-        try {
-          if (dxExecutable == null) {
-            String dxExecutableName = Utils.toolsDir().equals("windows") ? "dx.bat" : "dx";
-            dxExecutable = new File("tools/" + Utils.toolsDir() + "/dx/bin/" + dxExecutableName);
-          }
-          execSpec.setExecutable(dxExecutable);
-          execSpec.args("--dex");
-          execSpec.args("--output");
-          execSpec.args(destination.getCanonicalPath());
-          if (isDebug()) {
-            execSpec.args("--debug");
-          }
-          execSpec.args(source.getFiles());
-        } catch (IOException e) {
-          throw new UncheckedIOException(e);
-        }
-      }
-    });
-  }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/dx/DxTask.java b/buildSrc/src/main/java/dx/DxTask.java
new file mode 100644
index 0000000..d8975dc
--- /dev/null
+++ b/buildSrc/src/main/java/dx/DxTask.java
@@ -0,0 +1,125 @@
+// Copyright (c) 2019, 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 dx;
+
+import com.google.common.base.Optional;
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import javax.inject.Inject;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.workers.IsolationMode;
+import org.gradle.workers.WorkerExecutor;
+import utils.Utils;
+
+public class DxTask extends DefaultTask {
+
+  private final WorkerExecutor workerExecutor;
+
+  private FileCollection source;
+  private File destination;
+  private Optional<File> dxExecutable = Optional.absent(); // Worker API cannot handle null.
+  private boolean debug;
+
+  @Inject
+  public DxTask(WorkerExecutor workerExecutor) {
+    this.workerExecutor = workerExecutor;
+  }
+
+  @InputFiles
+  public FileCollection getSource() {
+    return source;
+  }
+
+  public void setSource(FileCollection source) {
+    this.source = source;
+  }
+
+  @OutputDirectory
+  public File getDestination() {
+    return destination;
+  }
+
+  public void setDestination(File destination) {
+    this.destination = destination;
+  }
+
+  @InputFile
+  @org.gradle.api.tasks.Optional
+  public File getDxExecutable() {
+    return dxExecutable.orNull();
+  }
+
+  public void setDxExecutable(File dxExecutable) {
+    this.dxExecutable = Optional.fromNullable(dxExecutable);
+  }
+
+  @Input
+  public boolean isDebug() {
+    return debug;
+  }
+
+  public void setDebug(boolean debug) {
+    this.debug = debug;
+  }
+
+  @TaskAction
+  void exec() {
+    workerExecutor.submit(RunDx.class, config -> {
+      config.setIsolationMode(IsolationMode.NONE);
+      config.params(source.getFiles(), destination, dxExecutable, debug);
+    });
+  }
+
+  public static class RunDx implements Runnable {
+    private final Set<File> sources;
+    private final File destination;
+    private final Optional<File> dxExecutable;
+    private final boolean debug;
+
+    @Inject
+    public RunDx(Set<File> sources, File destination, Optional<File> dxExecutable, boolean debug) {
+      this.sources = sources;
+      this.destination = destination;
+      this.dxExecutable = dxExecutable;
+      this.debug = debug;
+    }
+
+    @Override
+    public void run() {
+      try {
+        List<String> command = new ArrayList<>();
+        command.add(dxExecutable.or(Utils::dxExecutable).getCanonicalPath());
+        command.add("--dex");
+        command.add("--output");
+        command.add(destination.getCanonicalPath());
+        if (debug) {
+          command.add("--debug");
+        }
+        for (File source : sources) {
+          command.add(source.getCanonicalPath());
+        }
+
+        Process dx = new ProcessBuilder(command).inheritIO().start();
+        int exitCode = dx.waitFor();
+        if (exitCode != 0) {
+          throw new RuntimeException("dx failed with code " + exitCode);
+        }
+      } catch (IOException e) {
+        throw new UncheckedIOException(e);
+      } catch (InterruptedException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+}
diff --git a/buildSrc/src/main/java/smali/Smali.java b/buildSrc/src/main/java/smali/Smali.java
deleted file mode 100644
index 7a5241e..0000000
--- a/buildSrc/src/main/java/smali/Smali.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// 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 smali;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.gradle.api.DefaultTask;
-import org.gradle.api.UncheckedIOException;
-import org.gradle.api.file.FileTree;
-import org.gradle.api.tasks.InputFiles;
-import org.gradle.api.tasks.OutputDirectory;
-import org.gradle.api.tasks.OutputFile;
-import org.gradle.api.tasks.TaskAction;
-
-public class Smali extends DefaultTask {
-
-  private FileTree source;
-  private File destination;
-  private File smaliScript;
-
-  @InputFiles
-  public FileTree getSource() {
-    return source;
-  }
-
-  public void setSource(FileTree source) {
-    this.source = source;
-  }
-
-  @OutputFile
-  public File getDestination() {
-    return destination;
-  }
-
-  public void setDestination(File destination) {
-    this.destination = destination;
-  }
-
-  public File getSmaliScript() {
-    return smaliScript;
-  }
-
-  public void setSmaliScript(File smaliScript) {
-    this.smaliScript = smaliScript;
-  }
-
-  @TaskAction
-  void exec() {
-    try {
-      List<String> fileNames = source.getFiles().stream().map(file -> file.toString())
-          .collect(Collectors.toList());
-      org.jf.smali.SmaliOptions options = new org.jf.smali.SmaliOptions();
-      options.outputDexFile = destination.getCanonicalPath().toString();
-      org.jf.smali.Smali.assemble(options, fileNames);
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
-  }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/smali/SmaliTask.java b/buildSrc/src/main/java/smali/SmaliTask.java
new file mode 100644
index 0000000..845592e
--- /dev/null
+++ b/buildSrc/src/main/java/smali/SmaliTask.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2019, 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 smali;
+
+import static java.util.stream.Collectors.toList;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.List;
+import java.util.Set;
+import javax.inject.Inject;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.file.FileTree;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.workers.IsolationMode;
+import org.gradle.workers.WorkerExecutor;
+import org.jf.smali.Smali;
+import org.jf.smali.SmaliOptions;
+
+public class SmaliTask extends DefaultTask {
+
+  private final WorkerExecutor workerExecutor;
+
+  private FileTree source;
+  private File destination;
+
+  @Inject
+  public SmaliTask(WorkerExecutor workerExecutor) {
+    this.workerExecutor = workerExecutor;
+  }
+
+  @InputFiles
+  public FileTree getSource() {
+    return source;
+  }
+
+  public void setSource(FileTree source) {
+    this.source = source;
+  }
+
+  @OutputFile
+  public File getDestination() {
+    return destination;
+  }
+
+  public void setDestination(File destination) {
+    this.destination = destination;
+  }
+
+  @TaskAction
+  void exec() {
+    workerExecutor.submit(RunSmali.class, config -> {
+      config.setIsolationMode(IsolationMode.NONE);
+      config.params(source.getFiles(), destination);
+    });
+  }
+
+  public static class RunSmali implements Runnable {
+    private final Set<File> sources;
+    private final File destination;
+
+    @Inject
+    public RunSmali(Set<File> sources, File destination) {
+      this.sources = sources;
+      this.destination = destination;
+    }
+
+    @Override
+    public void run() {
+      try {
+        List<String> fileNames = sources.stream().map(File::toString).collect(toList());
+        SmaliOptions options = new SmaliOptions();
+        options.outputDexFile = destination.getCanonicalPath();
+        Smali.assemble(options, fileNames);
+      } catch (IOException e) {
+        throw new UncheckedIOException(e);
+      }
+    }
+  }
+}
diff --git a/buildSrc/src/main/java/utils/Utils.java b/buildSrc/src/main/java/utils/Utils.java
index 7158e80..7843edd 100644
--- a/buildSrc/src/main/java/utils/Utils.java
+++ b/buildSrc/src/main/java/utils/Utils.java
@@ -17,8 +17,13 @@
     }
   }
 
+  public static File dxExecutable() {
+    String dxExecutableName = Utils.toolsDir().equals("windows") ? "dx.bat" : "dx";
+    return new File("tools/" + Utils.toolsDir() + "/dx/bin/" + dxExecutableName);
+  }
+
   public static File dexMergerExecutable() {
     String executableName = Utils.toolsDir().equals("windows") ? "dexmerger.bat" : "dexmerger";
     return new File("tools/" + Utils.toolsDir() + "/dx/bin/" + executableName);
   }
-}
\ No newline at end of file
+}