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"));
+ }
+}