Add dex splitter support.

This is a very minimal first version. This adds support for running the tool on existing
dex code, supplying a mapping file that tells where the individual classes go.
This includes a simple parser for the mapping file, and a simply command line tool to run this.

Missing pieces:
  Proguard map support
  Mapping generation from jar inputs
  Optimized lookup

Change-Id: I7118dae25e8ce53be49ad1ebe70fbdd8fa7c0aa8
diff --git a/src/test/examples/dexsplitsample/Class1.java b/src/test/examples/dexsplitsample/Class1.java
new file mode 100644
index 0000000..c0cd25f
--- /dev/null
+++ b/src/test/examples/dexsplitsample/Class1.java
@@ -0,0 +1,15 @@
+// Copyright (c) 2018, 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 dexsplitsample;
+
+public class Class1 {
+  public static void main(String[] args) {
+    System.out.println("Class1");
+  }
+
+  protected String getClass1String() {
+    return "Class1String";
+  }
+}
diff --git a/src/test/examples/dexsplitsample/Class2.java b/src/test/examples/dexsplitsample/Class2.java
new file mode 100644
index 0000000..39d73b8
--- /dev/null
+++ b/src/test/examples/dexsplitsample/Class2.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2018, 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 dexsplitsample;
+
+public class Class2 {
+  public static void main(String[] args) {
+    System.out.println("Class2");
+  }
+}
diff --git a/src/test/examples/dexsplitsample/Class3.java b/src/test/examples/dexsplitsample/Class3.java
new file mode 100644
index 0000000..98469a5
--- /dev/null
+++ b/src/test/examples/dexsplitsample/Class3.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2018, 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 dexsplitsample;
+
+public class Class3 extends Class1 {
+  public static void main(String[] args) {
+    Class3 clazz = new Class3();
+    if (clazz.getClass1String() != "Class1String") {
+      throw new RuntimeException("Can't call method from super");
+    }
+    if (!new InnerClass().success()) {
+      throw new RuntimeException("Can't call method on inner class");
+    }
+    System.out.println("Class3");
+  }
+
+  private static class InnerClass {
+    public boolean success() {
+      return true;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
new file mode 100644
index 0000000..c803867
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
@@ -0,0 +1,107 @@
+// 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.dexsplitter;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class DexSplitterTests {
+
+  private static final String CLASS_DIR = ToolHelper.EXAMPLES_BUILD_DIR + "classes/dexsplitsample";
+  private static final String CLASS1_CLASS = CLASS_DIR + "/Class1.class";
+  private static final String CLASS2_CLASS = CLASS_DIR + "/Class2.class";
+  private static final String CLASS3_CLASS = CLASS_DIR + "/Class3.class";
+  private static final String CLASS3_INNER_CLASS = CLASS_DIR + "/Class3$InnerClass.class";
+
+  @Rule public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+  /**
+   * To test the file splitting we have 3 classes that we distribute like this: Class1 -> base
+   * Class2 -> feature1 Class3 -> feature1
+   *
+   * <p>Class1 and Class2 works independently of each other, but Class3 extends Class1, and
+   * therefore can't run without the base being loaded.
+   */
+  @Test
+  public void splitFiles() throws CompilationFailedException, IOException {
+    // Initial normal compile to create dex files.
+    Path inputDex = temp.newFolder().toPath().resolve("input.zip");
+    D8.run(
+        D8Command.builder()
+            .setOutput(inputDex, OutputMode.DexIndexed)
+            .addProgramFiles(Paths.get(CLASS1_CLASS))
+            .addProgramFiles(Paths.get(CLASS2_CLASS))
+            .addProgramFiles(Paths.get(CLASS3_CLASS))
+            .addProgramFiles(Paths.get(CLASS3_INNER_CLASS))
+            .build());
+
+    Path outputDex = temp.getRoot().toPath().resolve("output");
+    Path splitSpec = temp.getRoot().toPath().resolve("split_spec");
+    try (PrintWriter out = new PrintWriter(splitSpec.toFile(), "UTF-8")) {
+      out.write(
+          "dexsplitsample.Class1:base\n"
+              + "dexsplitsample.Class2:feature1\n"
+              + "dexsplitsample.Class3:feature1");
+    }
+
+    DexSplitter.main(
+        new String[] {
+          "--input", inputDex.toString(),
+          "--output", outputDex.toString(),
+          "--feature-splits", splitSpec.toString()
+        });
+
+    // Both classes should still work if we give all dex files to the system.
+    Path base = outputDex.getParent().resolve("output.base.zip");
+    Path feature = outputDex.getParent().resolve("output.feature1.zip");
+    for (String className : new String[] {"Class1", "Class2", "Class3"}) {
+      ArtCommandBuilder builder = new ArtCommandBuilder();
+      builder.appendClasspath(base.toString());
+      builder.appendClasspath(feature.toString());
+      builder.setMainClass("dexsplitsample." + className);
+      String out = ToolHelper.runArt(builder);
+      assertEquals(out, className + "\n");
+    }
+    // Individual classes should also work from the individual files.
+    String className = "Class1";
+    ArtCommandBuilder builder = new ArtCommandBuilder();
+    builder.appendClasspath(base.toString());
+    builder.setMainClass("dexsplitsample." + className);
+    String out = ToolHelper.runArt(builder);
+    assertEquals(out, className + "\n");
+
+    className = "Class2";
+    builder = new ArtCommandBuilder();
+    builder.appendClasspath(feature.toString());
+    builder.setMainClass("dexsplitsample." + className);
+    out = ToolHelper.runArt(builder);
+    assertEquals(out, className + "\n");
+
+    className = "Class3";
+    builder = new ArtCommandBuilder();
+    builder.appendClasspath(feature.toString());
+    builder.setMainClass("dexsplitsample." + className);
+    try {
+      ToolHelper.runArt(builder);
+      assertFalse(true);
+    } catch (AssertionError assertionError) {
+      // We expect this to throw since base is not in the path and Class3 depends on Class1
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java b/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
new file mode 100644
index 0000000..ba571fb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
@@ -0,0 +1,86 @@
+// 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 com.android.tools.r8.utils;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
+
+import com.android.tools.r8.utils.FeatureClassMapping.FeatureMappingException;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+
+public class FeatureClassMappingTest {
+
+  @Test
+  public void testSimpleParse() throws Exception {
+
+    List<String> lines =
+        ImmutableList.of(
+            "# Comment, don't care about contents: even more ::::",
+            "com.google.base:base",
+            "", // Empty lines allowed
+            "com.google.feature1:feature1",
+            "com.google.feature1:feature1", // Multiple definitions of the same predicate allowed.
+            "com.google$:feature1",
+            "_com.google:feature21",
+            "com.google.*:feature32");
+    FeatureClassMapping mapping = new FeatureClassMapping(lines);
+  }
+
+  private void ensureThrowsMappingException(List<String> lines) {
+    try {
+      new FeatureClassMapping(lines);
+      assertFalse(true);
+    } catch (FeatureMappingException e) {
+      // Expected
+    }
+  }
+
+  private void ensureThrowsMappingException(String string) {
+    ensureThrowsMappingException(ImmutableList.of(string));
+  }
+
+  @Test
+  public void testLookup() throws Exception {
+    List<String> lines =
+        ImmutableList.of(
+            "com.google.Base:base",
+            "",
+            "com.google.Feature1:feature1",
+            "com.google.Feature1:feature1", // Multiple definitions of the same predicate allowed.
+            "com.google.different.*:feature1",
+            "_com.Google:feature21",
+            "com.google.bas42.*:feature42");
+    FeatureClassMapping mapping = new FeatureClassMapping(lines);
+    assertEquals(mapping.featureForClass("com.google.Feature1"), "feature1");
+    assertEquals(mapping.featureForClass("com.google.different.Feature1"), "feature1");
+    assertEquals(mapping.featureForClass("com.google.different.Foobar"), "feature1");
+    assertEquals(mapping.featureForClass("com.google.Base"), "base");
+    assertEquals(mapping.featureForClass("com.google.bas42.foo.bar.bar.Foo"), "feature42");
+    assertEquals(mapping.featureForClass("com.google.bas42.f$o$o$.bar43.bar.Foo"), "feature42");
+    assertEquals(mapping.featureForClass("_com.Google"), "feature21");
+  }
+
+  @Test
+  public void testWrongLines() throws Exception {
+    // No colon.
+    ensureThrowsMappingException("foo");
+    ensureThrowsMappingException("com.google.base");
+    // Two colons.
+    ensureThrowsMappingException(ImmutableList.of("a:b:c"));
+    // Invalid identifiers.
+    ensureThrowsMappingException("12com.google.base:feature1");
+    ensureThrowsMappingException("$com.google.base:feature1");
+
+    // Empty identifier.
+    ensureThrowsMappingException("com..google:feature1");
+
+    // Ambiguous redefinition
+    ensureThrowsMappingException(
+        ImmutableList.of("com.google.foo:feature1", "com.google.foo:feature2"));
+    ensureThrowsMappingException(
+        ImmutableList.of("com.google.foo.*:feature1", "com.google.foo.*:feature2"));
+  }
+}