Desugared lib: Feature split support
Bug: 146329897
Change-Id: I08fd5cd22e616a490b1c301c390a3ddabfa735fc
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 5daaccc..7ba04b5 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -71,6 +71,7 @@
public final GraphLense graphLense;
public final NamingLens namingLens;
public final InternalOptions options;
+ private final CodeToKeep desugaredLibraryCodeToKeep;
public List<Marker> markers;
public List<DexString> markerStrings;
@@ -167,6 +168,7 @@
this.appView = appView;
assert options != null;
this.options = options;
+ this.desugaredLibraryCodeToKeep = CodeToKeep.createCodeToKeep(options, namingLens);
this.markers = markers;
this.graphLense = graphLense;
this.namingLens = namingLens;
@@ -310,6 +312,11 @@
}
// Wait for all files to be processed before moving on.
ThreadUtils.awaitFutures(dexDataFutures);
+ // A consumer can manage the generated keep rules.
+ if (options.desugaredLibraryKeepRuleConsumer != null && !desugaredLibraryCodeToKeep.isNop()) {
+ assert !options.isDesugaredLibraryCompilation();
+ desugaredLibraryCodeToKeep.generateKeepRules(options);
+ }
// Fail if there are pending errors, e.g., the program consumers may have reported errors.
options.reporter.failIfPendingErrors();
// Supply info to all additional resource consumers.
@@ -576,7 +583,14 @@
MethodToCodeObjectMapping codeMapping,
ByteBufferProvider provider) {
FileWriter fileWriter =
- new FileWriter(provider, objectMapping, codeMapping, application, options, namingLens);
+ new FileWriter(
+ provider,
+ objectMapping,
+ codeMapping,
+ application,
+ options,
+ namingLens,
+ desugaredLibraryCodeToKeep);
// Collect the non-fixed sections.
fileWriter.collect();
// Generate and write the bytes.
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index fa50d92..5cbd975 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -101,7 +101,8 @@
MethodToCodeObjectMapping codeMapping,
DexApplication application,
InternalOptions options,
- NamingLens namingLens) {
+ NamingLens namingLens,
+ CodeToKeep desugaredLibraryCodeToKeep) {
this.mapping = mapping;
this.codeMapping = codeMapping;
this.application = application;
@@ -109,7 +110,7 @@
this.namingLens = namingLens;
this.dest = new DexOutputBuffer(provider);
this.mixedSectionOffsets = new MixedSectionOffsets(options, codeMapping);
- this.desugaredLibraryCodeToKeep = CodeToKeep.createCodeToKeep(options, namingLens);
+ this.desugaredLibraryCodeToKeep = desugaredLibraryCodeToKeep;
}
public static void writeEncodedAnnotation(
@@ -223,12 +224,6 @@
writeSignature(layout);
writeChecksum(layout);
- // A consumer can manage the generated keep rules.
- if (options.desugaredLibraryKeepRuleConsumer != null && !desugaredLibraryCodeToKeep.isNop()) {
- assert !options.isDesugaredLibraryCompilation();
- desugaredLibraryCodeToKeep.generateKeepRules(options);
- }
-
// Wrap backing buffer with actual length.
return new ByteBufferResult(dest.stealByteBuffer(), layout.getEndOfFile());
}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/FeatureSplitTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/FeatureSplitTest.java
new file mode 100644
index 0000000..2f02755
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/FeatureSplitTest.java
@@ -0,0 +1,269 @@
+// 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 com.android.tools.r8.desugar.desugaredlibrary;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.dexsplitter.SplitterTestBase;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import dalvik.system.PathClassLoader;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class FeatureSplitTest extends DesugaredLibraryTestBase {
+
+ private final TestParameters parameters;
+ private final boolean shrinkDesugaredLibrary;
+ private final Class<?>[] baseAndFeatureClasses =
+ new Class<?>[] {BaseClass.class, FeatureClass.class, FeatureClass2.class};
+
+ @Parameters(name = "{1}, shrinkDesugaredLibrary: {0}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
+ }
+
+ public FeatureSplitTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+ this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testTwoFeatures() throws CompilationFailedException, IOException, ExecutionException {
+ CompiledWithFeature compiledWithFeature = new CompiledWithFeature().invoke(this);
+ Path basePath = compiledWithFeature.getBasePath();
+ Path feature1Path = compiledWithFeature.getFeature1Path();
+ Path feature2Path = compiledWithFeature.getFeature2Path();
+ Path desugaredLibrary = compiledWithFeature.getDesugaredLibrary();
+
+ assertKeepThe3StreamMethods(compiledWithFeature.getKeepRules());
+
+ assertClassPresent(basePath, BaseClass.class);
+ assertClassPresent(feature1Path, FeatureClass.class);
+ assertClassPresent(feature2Path, FeatureClass2.class);
+
+ verifyRun(BaseClass.class, basePath, desugaredLibrary, null, "42");
+ verifyRun(FeatureClass.class, basePath, desugaredLibrary, feature1Path, "1");
+ verifyRun(FeatureClass2.class, basePath, desugaredLibrary, feature2Path, "7");
+ }
+
+ private void assertKeepThe3StreamMethods(String keepRules) {
+ // Ensure count, toArray and forEach are kept.
+ assertTrue(
+ keepRules.contains(
+ "-keep class j$.lang.Iterable$-EL {\n"
+ + " void forEach(java.lang.Iterable, j$.util.function.Consumer);"));
+ assertTrue(
+ keepRules.contains(
+ "-keep class j$.util.stream.Stream {\n"
+ + " long count();\n"
+ + " java.lang.Object[] toArray();"));
+ }
+
+ private void assertClassPresent(Path basePath, Class<?> present)
+ throws IOException, ExecutionException {
+ CodeInspector inspector = new CodeInspector(basePath);
+ for (Class<?> clazz : baseAndFeatureClasses) {
+ if (clazz == present) {
+ assertTrue(inspector.clazz(present).isPresent());
+ } else {
+ assertFalse(inspector.clazz(present).isPresent());
+ }
+ }
+ }
+
+ private void verifyRun(
+ Class<?> toRun,
+ Path basePath,
+ Path desugaredLibrary,
+ Path splitterFeatureDexFile,
+ String expectedResult)
+ throws IOException {
+ ProcessResult result =
+ runFeatureOnArt(
+ toRun, desugaredLibrary, basePath, splitterFeatureDexFile, parameters.getRuntime());
+ assertEquals(result.exitCode, 0);
+ assertEquals(result.stdout, StringUtils.lines(expectedResult));
+ }
+
+ protected ProcessResult runFeatureOnArt(
+ Class toRun,
+ Path desugaredLibrary,
+ Path splitterBaseDexFile,
+ Path splitterFeatureDexFile,
+ TestRuntime runtime)
+ throws IOException {
+ assumeTrue(runtime.isDex());
+ ArtCommandBuilder commandBuilder = new ArtCommandBuilder(runtime.asDex().getVm());
+ commandBuilder.appendClasspath(splitterBaseDexFile.toString());
+ if (desugaredLibrary != null) {
+ commandBuilder.appendClasspath(desugaredLibrary.toString());
+ }
+ commandBuilder.appendProgramArgument(toRun.getName());
+ if (splitterFeatureDexFile != null) {
+ commandBuilder.appendProgramArgument(splitterFeatureDexFile.toString());
+ }
+ commandBuilder.setMainClass(SplitRunner.class.getName());
+ return ToolHelper.runArtRaw(commandBuilder);
+ }
+
+ public interface RunInterface {
+
+ void run();
+ }
+
+ // Base using ForEach.
+ public static class BaseClass implements RunInterface {
+
+ @Override
+ public void run() {
+ ArrayList<Integer> list = new ArrayList<>();
+ list.add(42);
+ list.forEach(System.out::println);
+ }
+ }
+
+ // Feature using count.
+ public static class FeatureClass implements RunInterface {
+
+ @SuppressWarnings("ReplaceInefficientStreamCount")
+ @Override
+ public void run() {
+ ArrayList<Object> list = new ArrayList<>();
+ list.add(new Object());
+ System.out.println(list.stream().count());
+ }
+ }
+
+ // Feature using toArray.
+ public static class FeatureClass2 implements RunInterface {
+
+ @SuppressWarnings("SimplifyStreamApiCallChains")
+ @Override
+ public void run() {
+ ArrayList<Integer> list = new ArrayList<>();
+ list.add(7);
+ System.out.println(list.stream().toArray()[0]);
+ }
+ }
+
+ static class SplitRunner {
+
+ /* We support two different modes:
+ * - One argument to main:
+ * Pass in the class to be loaded, must implement RunInterface, run will be called.
+ * - Two arguments to main:
+ * Pass in the class to be loaded, must implement RunInterface, run will be called.
+ * Pass in the feature split that we class load.
+ */
+ public static void main(String[] args) {
+ if (args.length < 1 || args.length > 2) {
+ throw new RuntimeException("Unsupported number of arguments");
+ }
+ String classToRun = args[0];
+ ClassLoader loader = SplitRunner.class.getClassLoader();
+ // In the case where we simulate splits, we pass in the feature as the second argument
+ if (args.length == 2) {
+ try {
+ loader = new PathClassLoader(args[1], SplitRunner.class.getClassLoader());
+ } catch (MalformedURLException e) {
+ throw new RuntimeException("Failed reading input URL");
+ }
+ }
+
+ try {
+ Class<?> aClass = loader.loadClass(classToRun);
+ RunInterface b = (RunInterface) aClass.newInstance();
+ b.run();
+ } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
+ throw new RuntimeException("Failed loading class");
+ }
+ }
+ }
+
+ private class CompiledWithFeature {
+
+ private Path basePath;
+ private Path feature1Path;
+ private Path feature2Path;
+ private Path desugaredLibrary;
+ private String keepRules = "";
+
+ public Path getBasePath() {
+ return basePath;
+ }
+
+ public Path getFeature1Path() {
+ return feature1Path;
+ }
+
+ public Path getFeature2Path() {
+ return feature2Path;
+ }
+
+ public Path getDesugaredLibrary() {
+ return desugaredLibrary;
+ }
+
+ public String getKeepRules() {
+ return keepRules;
+ }
+
+ public CompiledWithFeature invoke(FeatureSplitTest tester)
+ throws IOException, CompilationFailedException {
+
+ basePath = temp.newFile("base.zip").toPath();
+ feature1Path = temp.newFile("feature1.zip").toPath();
+ feature2Path = temp.newFile("feature2.zip").toPath();
+
+ KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+ testForR8(parameters.getBackend())
+ .addProgramClasses(BaseClass.class, RunInterface.class, SplitRunner.class)
+ .setMinApi(parameters.getApiLevel())
+ .addFeatureSplit(
+ builder ->
+ SplitterTestBase.simpleSplitProvider(
+ builder, feature1Path, temp, FeatureClass.class))
+ .addFeatureSplit(
+ builder ->
+ SplitterTestBase.simpleSplitProvider(
+ builder, feature2Path, temp, FeatureClass2.class))
+ .addKeepAllClassesRule()
+ .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+ .compile()
+ .writeToZip(basePath);
+ // Library desugaring is not needed above N.
+ if (parameters.getApiLevel().getLevel() > AndroidApiLevel.N.getLevel()) {
+ return this;
+ }
+ desugaredLibrary =
+ tester.buildDesugaredLibrary(
+ parameters.getApiLevel(), keepRuleConsumer.get(), shrinkDesugaredLibrary);
+ keepRules = keepRuleConsumer.get();
+ return this;
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
index fea2bce..1e31022 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
@@ -41,12 +41,12 @@
public class SplitterTestBase extends TestBase {
- protected static FeatureSplit simpleSplitProvider(
+ public static FeatureSplit simpleSplitProvider(
FeatureSplit.Builder builder, Path outputPath, TemporaryFolder temp, Class... classes) {
return simpleSplitProvider(builder, outputPath, temp, Arrays.asList(classes));
}
- protected static FeatureSplit simpleSplitProvider(
+ public static FeatureSplit simpleSplitProvider(
FeatureSplit.Builder builder,
Path outputPath,
TemporaryFolder temp,