Change DexSplitter API

Change output to be folder based. --output means folder to output to
Additionally, output the classes.dex files to folders instead of zip files

Allow name of output for the individual features to be specified (in AS they are all named the same)

Change-Id: I4ccd1254902ce653d42abbda2bae0e4e1074ebcb
diff --git a/src/main/java/com/android/tools/r8/DexSplitterHelper.java b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
index d28f93f..8573e0f 100644
--- a/src/main/java/com/android/tools/r8/DexSplitterHelper.java
+++ b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8;
 
-import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
+import com.android.tools.r8.DexIndexedConsumer.DirectoryConsumer;
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.dex.ApplicationWriter;
 import com.android.tools.r8.graph.AppInfo;
@@ -19,6 +19,8 @@
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.HashMap;
 import java.util.Map;
@@ -31,7 +33,7 @@
   public static void run(
       D8Command command,
       FeatureClassMapping featureClassMapping,
-      String outputArchive,
+      String output,
       String proguardMap)
       throws IOException, CompilationException, ExecutionException {
     InternalOptions options = command.getInternalOptions();
@@ -65,8 +67,12 @@
           AppInfo appInfo = new AppInfo(featureApp);
           featureApp = D8.optimize(featureApp, appInfo, options, timing, executor);
           // We create a specific consumer for each split.
-          DexIndexedConsumer consumer =
-              new ArchiveConsumer(Paths.get(outputArchive + "." + entry.getKey() + ".zip"));
+          Path outputDir = Paths.get(output).resolve(entry.getKey());
+          if (!Files.exists(outputDir)) {
+            Files.createDirectory(outputDir);
+          }
+          DexIndexedConsumer consumer = new DirectoryConsumer(outputDir);
+
           try {
             new ApplicationWriter(
                     featureApp,
diff --git a/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java b/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java
index e3753a7..5e8d54d 100644
--- a/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java
+++ b/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.utils.OptionsParsing.ParseContext;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
@@ -23,23 +24,55 @@
 
 public class DexSplitter {
 
-  private static final String DEFAULT_OUTPUT_ARCHIVE_FILENAME = "split";
+  private static final String DEFAULT_OUTPUT_DIR = "output";
 
   private static final boolean PRINT_ARGS = false;
 
+  public static class FeatureJar {
+    private String jar;
+    private String outputName;
+
+    public FeatureJar(String jar, String outputName) {
+      this.jar = jar;
+      this.outputName = outputName;
+    }
+
+    public FeatureJar(String jar) {
+      this(jar, featureNameFromJar(jar));
+    }
+
+    public String getJar() {
+      return jar;
+    }
+
+    public String getOutputName() {
+      return outputName;
+    }
+
+    private static String featureNameFromJar(String jar) {
+      Path jarPath = Paths.get(jar);
+      String featureName = jarPath.getFileName().toString();
+      if (featureName.endsWith(".jar") || featureName.endsWith(".zip")) {
+        featureName = featureName.substring(0, featureName.length() - 4);
+      }
+      return featureName;
+    }
+
+  }
+
   public static class Options {
     private List<String> inputArchives = new ArrayList<>();
-    private List<String> featureJars = new ArrayList<>();
-    private String splitBaseName = DEFAULT_OUTPUT_ARCHIVE_FILENAME;
+    private List<FeatureJar> featureJars = new ArrayList<>();
+    private String output = DEFAULT_OUTPUT_DIR;
     private String featureSplitMapping;
     private String proguardMap;
 
-    public String getSplitBaseName() {
-      return splitBaseName;
+    public String getOutput() {
+      return output;
     }
 
-    public void setSplitBaseName(String splitBaseName) {
-      this.splitBaseName = splitBaseName;
+    public void setOutput(String output) {
+      this.output = output;
     }
 
     public String getFeatureSplitMapping() {
@@ -62,19 +95,45 @@
       inputArchives.add(inputArchive);
     }
 
-    public void addFeatureJar(String featureJar) {
+    protected void addFeatureJar(FeatureJar featureJar) {
       featureJars.add(featureJar);
     }
 
+    public void addFeatureJar(String jar) {
+      featureJars.add(new FeatureJar(jar));
+    }
+
+    public void addFeatureJar(String jar, String outputName) {
+      featureJars.add(new FeatureJar(jar, outputName));
+    }
+
     public ImmutableList<String> getInputArchives() {
       return ImmutableList.copyOf(inputArchives);
     }
 
-    public ImmutableList<String> getFeatureJars() {
+    public ImmutableList<FeatureJar> getFeatureJars() {
       return ImmutableList.copyOf(featureJars);
     }
   }
 
+  /**
+   * Parse a feature jar argument and return the corresponding FeatureJar representation.
+   * Default to use the name of the jar file if the argument contains no ':', if the argument
+   * contains ':', then use the value after the ':' as the name.
+   * @param argument
+   * @return
+   */
+  private static FeatureJar parseFeatureJarArgument(String argument) {
+    if (argument.contains(":")) {
+      String[] parts = argument.split(":");
+      if (parts.length > 2) {
+        throw new RuntimeException("--feature-jar argument contains more than one :");
+      }
+      return new FeatureJar(parts[0], parts[1]);
+    }
+    return new FeatureJar(argument);
+  }
+
   private static Options parseArguments(String[] args) throws IOException {
     Options options = new Options();
     ParseContext context = new ParseContext(args);
@@ -86,12 +145,13 @@
       }
       List<String> featureJars = OptionsParsing.tryParseMulti(context, "--feature-jar");
       if (featureJars != null) {
-        featureJars.stream().forEach(options::addFeatureJar);
+        featureJars.stream().forEach(
+            (feature) -> options.addFeatureJar(parseFeatureJarArgument(feature)));
         continue;
       }
       String output = OptionsParsing.tryParseSingle(context, "--output", "-o");
       if (output != null) {
-        options.setSplitBaseName(output);
+        options.setOutput(output);
         continue;
       }
       String proguardMap = OptionsParsing.tryParseSingle(context, "--proguard-map", null);
@@ -114,8 +174,8 @@
     if (options.getFeatureSplitMapping() != null) {
       return FeatureClassMapping.fromSpecification(Paths.get(options.getFeatureSplitMapping()));
     }
-    assert !options.featureJars.isEmpty();
-    return FeatureClassMapping.fromJarFiles(options.featureJars);
+    assert !options.getFeatureJars().isEmpty();
+    return FeatureClassMapping.fromJarFiles(options.getFeatureJars());
   }
 
   private static void run(String[] args)
@@ -149,7 +209,7 @@
     FeatureClassMapping featureClassMapping = createFeatureClassMapping(options);
 
     DexSplitterHelper.run(
-        builder.build(), featureClassMapping, options.getSplitBaseName(), options.getProguardMap());
+        builder.build(), featureClassMapping, options.getOutput(), options.getProguardMap());
   }
 
   public static void main(String[] args) {
diff --git a/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java b/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
index 5b96201..6893eb1 100644
--- a/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
+++ b/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.ArchiveClassFileProvider;
+import com.android.tools.r8.dexsplitter.DexSplitter.FeatureJar;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -58,20 +59,15 @@
     return mapping;
   }
 
-  public static FeatureClassMapping fromJarFiles(List<String> jarFiles)
+  public static FeatureClassMapping fromJarFiles(List<FeatureJar> featureJars)
       throws FeatureMappingException, IOException {
     FeatureClassMapping mapping = new FeatureClassMapping();
-    for (String jar : jarFiles) {
-      Path jarPath = Paths.get(jar);
-      String featureName = jarPath.getFileName().toString();
-      if (featureName.endsWith(".jar") || featureName.endsWith(".zip")) {
-        featureName = featureName.substring(0, featureName.length() - 4);
-      }
-
+    for (FeatureJar featureJar : featureJars) {
+      Path jarPath = Paths.get(featureJar.getJar());
       ArchiveClassFileProvider provider = new ArchiveClassFileProvider(jarPath);
       for (String javaDescriptor : provider.getClassDescriptors()) {
           String javaType = DescriptorUtils.descriptorToJavaType(javaDescriptor);
-          mapping.addMapping(javaType, featureName);
+          mapping.addMapping(javaType, featureJar.getOutputName());
       }
     }
     return mapping;
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
index cf87d46..2db8fe0 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
@@ -84,14 +84,15 @@
             .addProgramFiles(Paths.get(CLASS4_LAMBDA_INTERFACE))
             .build());
 
-    Path output = temp.getRoot().toPath().resolve("output");
+    Path output = temp.newFolder().toPath().resolve("output");
+    Files.createDirectory(output);
     Path splitSpec = createSplitSpec();
 
     if (useOptions) {
       Options options = new Options();
       options.addInputArchive(inputZip.toString());
       options.setFeatureSplitMapping(splitSpec.toString());
-      options.setSplitBaseName(output.toString());
+      options.setOutput(output.toString());
       DexSplitter.run(options);
     } else {
       DexSplitter.main(
@@ -102,8 +103,8 @@
           });
     }
 
-    Path base = output.getParent().resolve("output.base.zip");
-    Path feature = output.getParent().resolve("output.feature1.zip");
+    Path base = output.resolve("base").resolve("classes.dex");
+    Path feature = output.resolve("feature1").resolve("classes.dex");
     validateUnobfuscatedOutput(base, feature);
   }
 
@@ -201,7 +202,8 @@
             .addProgramFiles(Paths.get(CLASS4_LAMBDA_INTERFACE))
             .build());
 
-    Path output = temp.getRoot().toPath().resolve("output");
+    Path output = temp.newFolder().toPath().resolve("output");
+    Files.createDirectory(output);
     Path baseJar = temp.getRoot().toPath().resolve("base.jar");
     Path featureJar = temp.getRoot().toPath().resolve("feature1.jar");
     ZipOutputStream baseStream = new ZipOutputStream(Files.newOutputStream(baseJar));
@@ -233,14 +235,16 @@
     featureStream.write(Files.readAllBytes(Paths.get(CLASS4_LAMBDA_INTERFACE)));
     featureStream.closeEntry();
     featureStream.close();
+    // Make sure that we can pass in a name for the output.
+    String specificOutputName = "renamed";
     if (useOptions) {
       Options options = new Options();
       options.addInputArchive(inputZip.toString());
-      options.setSplitBaseName(output.toString());
+      options.setOutput(output.toString());
       if (explicitBase) {
         options.addFeatureJar(baseJar.toString());
       }
-      options.addFeatureJar(featureJar.toString());
+      options.addFeatureJar(featureJar.toString(), specificOutputName);
       DexSplitter.run(options);
     } else {
       List<String> args = Lists.newArrayList(
@@ -249,7 +253,7 @@
           "--output",
           output.toString(),
           "--feature-jar",
-          featureJar.toString());
+          featureJar.toString().concat(":").concat(specificOutputName));
       if (explicitBase) {
         args.add("--feature-jar");
         args.add(baseJar.toString());
@@ -257,8 +261,8 @@
 
       DexSplitter.main(args.toArray(new String[0]));
     }
-    Path base = output.getParent().resolve("output.base.zip");
-    Path feature = output.getParent().resolve("output.feature1.zip");
+    Path base = output.resolve("base").resolve("classes.dex");
+    Path feature = output.resolve(specificOutputName).resolve("classes.dex");;
     validateUnobfuscatedOutput(base, feature);
   }
 
@@ -284,7 +288,8 @@
             .addProguardConfiguration(getProguardConf(), null)
             .build());
 
-    Path outputDex = temp.getRoot().toPath().resolve("output");
+    Path outputDex = temp.newFolder().toPath().resolve("output");
+    Files.createDirectory(outputDex);
     Path splitSpec = createSplitSpec();
 
     DexSplitter.main(
@@ -295,8 +300,8 @@
           "--proguard-map", proguardMap.toString()
         });
 
-    Path base = outputDex.getParent().resolve("output.base.zip");
-    Path feature = outputDex.getParent().resolve("output.feature1.zip");
+    Path base = outputDex.resolve("base").resolve("classes.dex");
+    Path feature = outputDex.resolve("feature1").resolve("classes.dex");
     String class3 = "dexsplitsample.Class3";
     // We should still be able to run the Class3 which we kept, it has a call to the obfuscated
     // class1 which is in base.