Add CLI support for feature splits

Bug: 156854830
Change-Id: I0288f4c535fdb675eeecdd28d2028120973df6fc
diff --git a/src/main/java/com/android/tools/r8/R8CommandParser.java b/src/main/java/com/android/tools/r8/R8CommandParser.java
index d31606d..6220eb4 100644
--- a/src/main/java/com/android/tools/r8/R8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/R8CommandParser.java
@@ -11,7 +11,11 @@
 import com.google.common.collect.Iterables;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public class R8CommandParser extends BaseCompilerCommandParser<R8Command, R8Command.Builder> {
@@ -24,12 +28,15 @@
           MIN_API_FLAG,
           "--main-dex-rules",
           "--main-dex-list",
+          "--feature",
           "--main-dex-list-output",
           "--pg-conf",
           "--pg-map-output",
           "--desugared-lib",
           THREAD_COUNT_FLAG);
 
+  private static final Set<String> OPTIONS_WITH_TWO_PARAMETERS = ImmutableSet.of("--feature");
+
   public static void main(String[] args) throws CompilationFailedException {
     R8Command command = parse(args, Origin.root()).build();
     if (command.isPrintHelp()) {
@@ -83,6 +90,9 @@
                   "  --main-dex-rules <file> # Proguard keep rules for classes to place in the",
                   "                          # primary dex file.",
                   "  --main-dex-list <file>  # List of classes to place in the primary dex file.",
+                  "  --feature <input> <output> ",
+                  "                          # Add feature <input> file to <output> file. Several ",
+                  "                          # occurrences can map to the same output.",
                   "  --main-dex-list-output <file>  ",
                   "                          # Output the full main-dex list in <file>."),
               ASSERTIONS_USAGE_MESSAGE,
@@ -131,9 +141,11 @@
   private void parse(
       String[] args, Origin argsOrigin, R8Command.Builder builder, ParseState state) {
     String[] expandedArgs = FlagFile.expandFlagFiles(args, builder::error);
+    Map<Path, List<Path>> featureSplitJars = new HashMap<>();
     for (int i = 0; i < expandedArgs.length; i++) {
       String arg = expandedArgs[i].trim();
       String nextArg = null;
+      String nextNextArg = null;
       if (OPTIONS_WITH_PARAMETER.contains(arg)) {
         if (++i < expandedArgs.length) {
           nextArg = expandedArgs[i];
@@ -143,6 +155,16 @@
                   "Missing parameter for " + expandedArgs[i - 1] + ".", argsOrigin));
           break;
         }
+        if (OPTIONS_WITH_TWO_PARAMETERS.contains(arg)) {
+          if (++i < expandedArgs.length) {
+            nextNextArg = expandedArgs[i];
+          } else {
+            builder.error(
+                new StringDiagnostic(
+                    "Missing parameter for " + expandedArgs[i - 2] + ".", argsOrigin));
+            break;
+          }
+        }
       }
       if (arg.length() == 0) {
         continue;
@@ -214,6 +236,10 @@
         builder.setDisableDesugaring(true);
       } else if (arg.equals("--main-dex-rules")) {
         builder.addMainDexRulesFiles(Paths.get(nextArg));
+      } else if (arg.equals("--feature")) {
+        featureSplitJars
+            .computeIfAbsent(Paths.get(nextNextArg), k -> new ArrayList<>())
+            .add(Paths.get(nextArg));
       } else if (arg.equals("--main-dex-list")) {
         builder.addMainDexListFiles(Paths.get(nextArg));
       } else if (arg.equals("--main-dex-list-output")) {
@@ -239,5 +265,20 @@
         builder.addProgramFiles(Paths.get(arg));
       }
     }
+    featureSplitJars.forEach(
+        (outputPath, inputJars) -> addFeatureJar(builder, outputPath, inputJars));
+  }
+
+  public void addFeatureJar(R8Command.Builder builder, Path outputPath, List<Path> inputJarPaths) {
+    builder.addFeatureSplit(
+        featureSplitGenerator -> {
+          featureSplitGenerator.setProgramConsumer(
+              builder.createProgramOutputConsumer(outputPath, OutputMode.DexIndexed, true));
+          for (Path inputPath : inputJarPaths) {
+            featureSplitGenerator.addProgramResourceProvider(
+                ArchiveProgramResourceProvider.fromArchive(inputPath));
+          }
+          return featureSplitGenerator.build();
+        });
   }
 }
diff --git a/src/test/java/com/android/tools/r8/R8CommandTest.java b/src/test/java/com/android/tools/r8/R8CommandTest.java
index 4f8806c..b5c518c 100644
--- a/src/test/java/com/android/tools/r8/R8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/R8CommandTest.java
@@ -120,6 +120,52 @@
   }
 
   @Test
+  public void passFeatureSplit() throws Throwable {
+    Path working = temp.getRoot().toPath();
+    Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar").toAbsolutePath();
+    Path inputFeature = Paths.get(EXAMPLES_BUILD_DIR, "arrayaccess.jar").toAbsolutePath();
+    Path library = ToolHelper.getDefaultAndroidJar();
+    Path output = working.resolve("classes.dex");
+    Path featureOutput = working.resolve("feature.zip");
+    assertFalse(Files.exists(output));
+    assertFalse(Files.exists(featureOutput));
+    ProcessResult result =
+        ToolHelper.forkR8(
+            working,
+            input.toString(),
+            "--lib",
+            library.toAbsolutePath().toString(),
+            "--feature",
+            inputFeature.toAbsolutePath().toString(),
+            featureOutput.toAbsolutePath().toString(),
+            "--no-tree-shaking");
+    assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode);
+    assertTrue(Files.exists(output));
+    assertTrue(Files.exists(featureOutput));
+  }
+
+  @Test
+  public void featureOnlyOneArgument() throws Throwable {
+    Path working = temp.getRoot().toPath();
+    Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar").toAbsolutePath();
+    Path inputFeature = Paths.get(EXAMPLES_BUILD_DIR, "arrayaccess.jar").toAbsolutePath();
+    Path library = ToolHelper.getDefaultAndroidJar();
+    Path output = working.resolve("classes.dex");
+    assertFalse(Files.exists(output));
+    ProcessResult result =
+        ToolHelper.forkR8(
+            working,
+            input.toString(),
+            "--lib",
+            library.toAbsolutePath().toString(),
+            "--no-tree-shaking",
+            "--feature",
+            inputFeature.toAbsolutePath().toString());
+    assertNotEquals("R8 run failed: " + result.stderr, 0, result.exitCode);
+    assertTrue(result.stderr.contains("Missing parameter for"));
+  }
+
+  @Test
   public void flagsFile() throws Throwable {
     Path working = temp.getRoot().toPath();
     Path library = ToolHelper.getDefaultAndroidJar();