Add output functionality for the R8 with feature splits.
This enables is the first cl enabling output similar to what the
dexsplitter would (i.e., we still can inline across the boundary)
Bug: 122902374
Change-Id: Ib3613197e4e5e60e38a4995eb1ca54ae70e63bf1
diff --git a/src/main/java/com/android/tools/r8/FeatureSplit.java b/src/main/java/com/android/tools/r8/FeatureSplit.java
index 011fc25..5947786 100644
--- a/src/main/java/com/android/tools/r8/FeatureSplit.java
+++ b/src/main/java/com/android/tools/r8/FeatureSplit.java
@@ -38,11 +38,11 @@
this.programResourceProviders = programResourceProviders;
}
- List<ProgramResourceProvider> getProgramResourceProviders() {
+ public List<ProgramResourceProvider> getProgramResourceProviders() {
return programResourceProviders;
}
- ProgramConsumer getProgramConsumer() {
+ public ProgramConsumer getProgramConsumer() {
return programConsumer;
}
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 235882e..c986011 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -6,6 +6,7 @@
import com.android.tools.r8.ProgramResource.Kind;
import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
+import com.android.tools.r8.features.FeatureSplitConfiguration;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
import com.android.tools.r8.origin.Origin;
@@ -364,7 +365,9 @@
*/
public Builder addFeatureSplit(
Function<FeatureSplit.Builder, FeatureSplit> featureSplitGenerator) {
- featureSplits.add(featureSplitGenerator.apply(FeatureSplit.builder(getReporter())));
+ FeatureSplit featureSplit = featureSplitGenerator.apply(FeatureSplit.builder(getReporter()));
+ featureSplits.add(featureSplit);
+ featureSplit.getProgramResourceProviders().forEach(this::addProgramResourceProvider);
return self();
}
@@ -516,6 +519,9 @@
boolean desugaring =
(getProgramConsumer() instanceof ClassFileConsumer) ? false : !getDisableDesugaring();
+ FeatureSplitConfiguration featureSplitConfiguration =
+ !featureSplits.isEmpty() ? new FeatureSplitConfiguration(featureSplits, reporter) : null;
+
R8Command command =
new R8Command(
getAppBuilder().build(),
@@ -544,7 +550,7 @@
getDexClassChecksumFilter(),
desugaredLibraryKeepRuleConsumer,
libraryConfiguration,
- featureSplits);
+ featureSplitConfiguration);
return command;
}
@@ -626,7 +632,7 @@
private final Consumer<List<ProguardConfigurationRule>> syntheticProguardRulesConsumer;
private final StringConsumer desugaredLibraryKeepRuleConsumer;
private final DesugaredLibraryConfiguration libraryConfiguration;
- private final List<FeatureSplit> featureSplits;
+ private final FeatureSplitConfiguration featureSplitConfiguration;
/** Get a new {@link R8Command.Builder}. */
public static Builder builder() {
@@ -702,7 +708,7 @@
BiPredicate<String, Long> dexClassChecksumFilter,
StringConsumer desugaredLibraryKeepRuleConsumer,
DesugaredLibraryConfiguration libraryConfiguration,
- List<FeatureSplit> featureSplits) {
+ FeatureSplitConfiguration featureSplitConfiguration) {
super(
inputApp,
mode,
@@ -732,7 +738,7 @@
this.syntheticProguardRulesConsumer = syntheticProguardRulesConsumer;
this.desugaredLibraryKeepRuleConsumer = desugaredLibraryKeepRuleConsumer;
this.libraryConfiguration = libraryConfiguration;
- this.featureSplits = featureSplits;
+ this.featureSplitConfiguration = featureSplitConfiguration;
}
private R8Command(boolean printHelp, boolean printVersion) {
@@ -753,7 +759,7 @@
syntheticProguardRulesConsumer = null;
desugaredLibraryKeepRuleConsumer = null;
libraryConfiguration = null;
- featureSplits = null;
+ featureSplitConfiguration = null;
}
/** Get the enable-tree-shaking state. */
@@ -850,6 +856,8 @@
internal.proguardCompatibilityRulesOutput = proguardCompatibilityRulesOutput;
internal.dataResourceConsumer = internal.programConsumer.getDataResourceConsumer();
+ internal.featureSplitConfiguration = featureSplitConfiguration;
+
internal.syntheticProguardRulesConsumer = syntheticProguardRulesConsumer;
// Default is to remove Java assertion code as Dalvik and Art does not reliable support
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 431805b..77a5da9 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -306,8 +306,16 @@
consumer = options.getDexFilePerClassFileConsumer();
byteBufferProvider = options.getDexFilePerClassFileConsumer();
} else {
- consumer = options.getDexIndexedConsumer();
- byteBufferProvider = options.getDexIndexedConsumer();
+ if (virtualFile.getFeatureSplit() != null) {
+ ProgramConsumer featureConsumer =
+ virtualFile.getFeatureSplit().getProgramConsumer();
+ assert featureConsumer instanceof DexIndexedConsumer;
+ consumer = featureConsumer;
+ byteBufferProvider = (DexIndexedConsumer) featureConsumer;
+ } else {
+ consumer = options.getDexIndexedConsumer();
+ byteBufferProvider = options.getDexIndexedConsumer();
+ }
}
ObjectToOffsetMapping objectMapping = virtualFile.computeMapping(application);
MethodToCodeObjectMapping codeMapping =
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index 1e2a264..7b18fd8 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.dex;
+import com.android.tools.r8.FeatureSplit;
import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
import com.android.tools.r8.errors.InternalCompilerError;
import com.android.tools.r8.graph.DexApplication;
@@ -26,6 +27,7 @@
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterators;
import com.google.common.collect.Sets;
import java.io.IOException;
@@ -69,18 +71,29 @@
private final int id;
private final VirtualFileIndexedItemCollection indexedItems;
private final IndexedItemTransaction transaction;
+ private final FeatureSplit featureSplit;
private final DexProgramClass primaryClass;
VirtualFile(int id, NamingLens namingLens) {
- this(id, namingLens, null);
+ this(id, namingLens, null, null);
+ }
+
+ VirtualFile(int id, NamingLens namingLens, FeatureSplit featureSplit) {
+ this(id, namingLens, null, featureSplit);
}
private VirtualFile(int id, NamingLens namingLens, DexProgramClass primaryClass) {
+ this(id, namingLens, primaryClass, null);
+ }
+
+ private VirtualFile(
+ int id, NamingLens namingLens, DexProgramClass primaryClass, FeatureSplit featureSplit) {
this.id = id;
this.indexedItems = new VirtualFileIndexedItemCollection(namingLens);
this.transaction = new IndexedItemTransaction(indexedItems, namingLens);
this.primaryClass = primaryClass;
+ this.featureSplit = featureSplit;
}
public int getId() {
@@ -96,6 +109,10 @@
return classDescriptors;
}
+ public FeatureSplit getFeatureSplit() {
+ return featureSplit;
+ }
+
public String getPrimaryClassDescriptor() {
return primaryClass == null ? null : primaryClass.type.descriptor.toString();
}
@@ -131,12 +148,13 @@
}
private static Map<DexProgramClass, String> computeOriginalNameMapping(
- Collection<DexProgramClass> classes,
- ClassNameMapper proguardMap) {
- Map<DexProgramClass, String> originalNames = new HashMap<>();
- classes.forEach((DexProgramClass c) ->
- originalNames.put(c,
- DescriptorUtils.descriptorToJavaType(c.type.toDescriptorString(), proguardMap)));
+ Collection<DexProgramClass> classes, ClassNameMapper proguardMap) {
+ Map<DexProgramClass, String> originalNames = new HashMap<>(classes.size());
+ classes.forEach(
+ (DexProgramClass c) -> {
+ originalNames.put(
+ c, DescriptorUtils.descriptorToJavaType(c.type.toDescriptorString(), proguardMap));
+ });
return originalNames;
}
@@ -367,6 +385,55 @@
sortedClasses.addAll(classes);
return sortedClasses;
}
+
+ protected Map<FeatureSplit, Set<DexProgramClass>> removeFeatureSplitClassesGetMapping() {
+ if (options.featureSplitConfiguration == null) {
+ return ImmutableMap.of();
+ }
+
+ // Pull out the classes that should go into feature splits.
+ Map<FeatureSplit, Set<DexProgramClass>> featureSplitClasses =
+ options.featureSplitConfiguration.getFeatureSplitClasses(
+ classes, application.getProguardMap());
+ if (featureSplitClasses.size() > 0) {
+ for (Set<DexProgramClass> featureClasses : featureSplitClasses.values()) {
+ classes.removeAll(featureClasses);
+ }
+ }
+ return featureSplitClasses;
+ }
+
+ protected void addFeatureSplitFiles(Map<FeatureSplit, Set<DexProgramClass>> featureSplitClasses)
+ throws IOException {
+ addFeatureSplitFiles(featureSplitClasses, FillStrategy.FILL_MAX);
+ }
+
+ protected void addFeatureSplitFiles(
+ Map<FeatureSplit, Set<DexProgramClass>> featureSplitClasses, FillStrategy fillStrategy)
+ throws IOException {
+ if (featureSplitClasses.isEmpty()) {
+ return;
+ }
+ List<VirtualFile> filesForDistribution;
+ for (Map.Entry<FeatureSplit, Set<DexProgramClass>> featureSplitSetEntry :
+ featureSplitClasses.entrySet()) {
+ // Add a new virtual file, start from index 0 again
+ virtualFiles.add(new VirtualFile(0, writer.namingLens, featureSplitSetEntry.getKey()));
+ Set<DexProgramClass> featureClasses =
+ sortClassesByPackage(featureSplitSetEntry.getValue(), originalNames);
+ filesForDistribution = virtualFiles.subList(virtualFiles.size() - 1, virtualFiles.size());
+
+ new PackageSplitPopulator(
+ filesForDistribution,
+ featureClasses,
+ originalNames,
+ application.dexItemFactory,
+ fillStrategy,
+ 0,
+ writer.namingLens)
+ .call();
+ }
+ }
}
public static class FillFilesDistributor extends DistributorBase {
@@ -402,6 +469,9 @@
fileIndexOffset = 1;
}
+ Map<FeatureSplit, Set<DexProgramClass>> featureSplitClasses =
+ removeFeatureSplitClassesGetMapping();
+
if (multidexLegacy && options.enableInheritanceClassInDexDistributor) {
new InheritanceClassInDexDistributor(mainDexFile, filesForDistribution, classes,
originalNames, fileIndexOffset, writer.namingLens, writer.application, executorService)
@@ -415,6 +485,8 @@
fillStrategy, fileIndexOffset, writer.namingLens)
.call();
}
+ addFeatureSplitFiles(featureSplitClasses, fillStrategy);
+
assert totalClassNumber == virtualFiles.stream().mapToInt(dex -> dex.classes().size()).sum();
return virtualFiles;
}
@@ -427,12 +499,20 @@
@Override
public Collection<VirtualFile> run() throws ExecutionException, IOException {
+ Map<FeatureSplit, Set<DexProgramClass>> featureSplitClasses =
+ removeFeatureSplitClassesGetMapping();
// Add all classes to the main dex file.
for (DexProgramClass programClass : classes) {
mainDexFile.addClass(programClass);
}
mainDexFile.commitTransaction();
mainDexFile.throwIfFull(false, options.reporter);
+ if (options.featureSplitConfiguration != null) {
+ if (!featureSplitClasses.isEmpty()) {
+ // TODO(141334414): Figure out if we allow multidex in features even when mono-dexing
+ addFeatureSplitFiles(featureSplitClasses);
+ }
+ }
return virtualFiles;
}
}
@@ -678,12 +758,16 @@
private int nextFileId;
private Iterator<VirtualFile> allFilesCyclic;
private Iterator<VirtualFile> activeFiles;
+ private FeatureSplit featuresplit;
VirtualFileCycler(List<VirtualFile> files, NamingLens namingLens, int fileIndexOffset) {
this.files = files;
this.namingLens = namingLens;
nextFileId = files.size() + fileIndexOffset;
+ if (files.size() > 0) {
+ featuresplit = files.get(0).getFeatureSplit();
+ }
reset();
}
@@ -708,7 +792,7 @@
if (hasNext()) {
return activeFiles.next();
} else {
- VirtualFile newFile = new VirtualFile(nextFileId++, namingLens);
+ VirtualFile newFile = new VirtualFile(nextFileId++, namingLens, featuresplit);
files.add(newFile);
allFilesCyclic = Iterators.cycle(files);
return newFile;
@@ -739,7 +823,7 @@
}
VirtualFile addFile() {
- VirtualFile newFile = new VirtualFile(nextFileId++, namingLens);
+ VirtualFile newFile = new VirtualFile(nextFileId++, namingLens, featuresplit);
files.add(newFile);
reset();
diff --git a/src/main/java/com/android/tools/r8/features/FeatureSplitConfiguration.java b/src/main/java/com/android/tools/r8/features/FeatureSplitConfiguration.java
new file mode 100644
index 0000000..8e779f4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/features/FeatureSplitConfiguration.java
@@ -0,0 +1,81 @@
+// 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.features;
+
+import com.android.tools.r8.FeatureSplit;
+import com.android.tools.r8.ProgramResource;
+import com.android.tools.r8.ProgramResourceProvider;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.Reporter;
+import com.google.common.collect.Sets;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class FeatureSplitConfiguration {
+ private final List<FeatureSplit> featureSplits;
+
+ private final Map<String, FeatureSplit> javaTypeToFeatureSplitMapping = new HashMap<>();
+
+ public FeatureSplitConfiguration(List<FeatureSplit> featureSplits, Reporter reporter) {
+ this.featureSplits = featureSplits;
+ for (FeatureSplit featureSplit : featureSplits) {
+ for (ProgramResourceProvider programResourceProvider :
+ featureSplit.getProgramResourceProviders()) {
+ try {
+ for (ProgramResource programResource : programResourceProvider.getProgramResources()) {
+ for (String classDescriptor : programResource.getClassDescriptors()) {
+ javaTypeToFeatureSplitMapping.put(
+ DescriptorUtils.descriptorToJavaType(classDescriptor), featureSplit);
+ }
+ }
+ } catch (ResourceException e) {
+ throw reporter.fatalError(e.getMessage());
+ }
+ }
+ }
+ }
+
+ public Map<FeatureSplit, Set<DexProgramClass>> getFeatureSplitClasses(
+ Set<DexProgramClass> classes, ClassNameMapper mapper) {
+ Map<FeatureSplit, Set<DexProgramClass>> result = new IdentityHashMap<>();
+
+ for (DexProgramClass programClass : classes) {
+ String originalClassName =
+ DescriptorUtils.descriptorToJavaType(programClass.type.toDescriptorString(), mapper);
+ FeatureSplit featureSplit = javaTypeToFeatureSplitMapping.get(originalClassName);
+ if (featureSplit != null) {
+ result.computeIfAbsent(featureSplit, f -> Sets.newIdentityHashSet()).add(programClass);
+ }
+ }
+ return result;
+ }
+
+ public boolean inSameFeatureOrBase(DexMethod a, DexMethod b) {
+ return inSameFeatureOrBase(a.holder, b.holder);
+ }
+
+ public boolean inSameFeatureOrBase(DexType a, DexType b) {
+ assert a.isClassType() && b.isClassType();
+ if (javaTypeToFeatureSplitMapping.isEmpty()) {
+ return true;
+ }
+ // TODO(ricow): Consider caching the descriptor string.
+ return javaTypeToFeatureSplitMapping.get(
+ DescriptorUtils.descriptorToJavaType(a.toDescriptorString()))
+ == javaTypeToFeatureSplitMapping.get(
+ DescriptorUtils.descriptorToJavaType(b.toDescriptorString()));
+ }
+
+ public List<FeatureSplit> getFeatureSplits() {
+ return featureSplits;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index dbd4989..d202fd8 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -8,10 +8,10 @@
import com.android.tools.r8.ClassFileConsumer;
import com.android.tools.r8.CompilationMode;
import com.android.tools.r8.DataResourceConsumer;
-import com.android.tools.r8.DataResourceProvider;
import com.android.tools.r8.DesugarGraphConsumer;
import com.android.tools.r8.DexFilePerClassFileConsumer;
import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.FeatureSplit;
import com.android.tools.r8.ProgramConsumer;
import com.android.tools.r8.StringConsumer;
import com.android.tools.r8.Version;
@@ -23,6 +23,7 @@
import com.android.tools.r8.errors.InvalidDebugInfoException;
import com.android.tools.r8.errors.MissingNestHostNestDesugarDiagnostic;
import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
+import com.android.tools.r8.features.FeatureSplitConfiguration;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
@@ -103,8 +104,8 @@
// TODO(zerny): Make this private-final once we have full program-consumer support.
public ProgramConsumer programConsumer = null;
- public final List<DataResourceProvider> dataResourceProviders = new ArrayList<>();
public DataResourceConsumer dataResourceConsumer;
+ public FeatureSplitConfiguration featureSplitConfiguration;
// Constructor for testing and/or other utilities.
public InternalOptions() {
@@ -352,6 +353,11 @@
dataResourceConsumer.finished(reporter);
}
}
+ if (featureSplitConfiguration != null) {
+ for (FeatureSplit featureSplit : featureSplitConfiguration.getFeatureSplits()) {
+ featureSplit.getProgramConsumer().finished(reporter);
+ }
+ }
}
public boolean shouldDesugarNests() {
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterInlineRegression.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterInlineRegression.java
index 1d26f28..cd9ad87 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterInlineRegression.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterInlineRegression.java
@@ -6,7 +6,9 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.NeverMerge;
import com.android.tools.r8.R8FullTestBuilder;
import com.android.tools.r8.R8TestCompileResult;
@@ -69,6 +71,25 @@
assertTrue(processResult.stderr.contains("NoClassDefFoundError"));
}
+ @Test
+ public void testOnR8Splitter() throws IOException, CompilationFailedException {
+ assumeTrue(parameters.isDexRuntime());
+ Consumer<R8FullTestBuilder> configurator =
+ r8FullTestBuilder -> r8FullTestBuilder.enableMergeAnnotations().noMinification();
+ ProcessResult processResult =
+ testR8Splitter(
+ parameters,
+ ImmutableSet.of(BaseSuperClass.class),
+ ImmutableSet.of(FeatureClass.class),
+ FeatureClass.class,
+ EXPECTED,
+ a -> true,
+ configurator);
+ // TODO(122902374): This should not fail, but currently we are just on par with DexSplitter.
+ assertNotEquals(processResult.exitCode, 0);
+ assertTrue(processResult.stderr.contains("NoClassDefFoundError"));
+ }
+
@NeverMerge
public abstract static class BaseSuperClass implements RunInterface {
public void run() {
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/R8FeatureSplitTest.java b/src/test/java/com/android/tools/r8/dexsplitter/R8FeatureSplitTest.java
index dce45bf..04d3b14 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/R8FeatureSplitTest.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/R8FeatureSplitTest.java
@@ -3,18 +3,20 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.dexsplitter;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertTrue;
+
import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.DexIndexedConsumer;
-import com.android.tools.r8.DiagnosticsHandler;
import com.android.tools.r8.FeatureSplit;
-import com.android.tools.r8.ProgramResource;
-import com.android.tools.r8.ProgramResourceProvider;
-import com.android.tools.r8.ResourceException;
-import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
import java.io.IOException;
-import java.util.Collection;
+import java.nio.file.Path;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -22,7 +24,7 @@
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
-public class R8FeatureSplitTest extends TestBase {
+public class R8FeatureSplitTest extends SplitterTestBase {
private static String EXPECTED = "Hello world";
@@ -39,18 +41,8 @@
private static FeatureSplit emptySplitProvider(FeatureSplit.Builder builder) {
builder
- .addProgramResourceProvider(
- new ProgramResourceProvider() {
- @Override
- public Collection<ProgramResource> getProgramResources() throws ResourceException {
- return null;
- }
- })
- .setProgramConsumer(
- new DexIndexedConsumer() {
- @Override
- public void finished(DiagnosticsHandler handler) {}
- });
+ .addProgramResourceProvider(() -> ImmutableList.of())
+ .setProgramConsumer(DexIndexedConsumer.emptyConsumer());
return builder.build();
}
@@ -66,9 +58,85 @@
.assertSuccessWithOutputLines(EXPECTED);
}
+ @Test
+ public void testTwoFeatures() throws CompilationFailedException, IOException, ExecutionException {
+ Path basePath = temp.newFile("base.zip").toPath();
+ Path feature1Path = temp.newFile("feature1.zip").toPath();
+ Path feature2Path = temp.newFile("feature2.zip").toPath();
+
+ testForR8(parameters.getBackend())
+ .addProgramClasses(BaseClass.class, RunInterface.class, SplitRunner.class)
+ .setMinApi(parameters.getRuntime())
+ .addFeatureSplit(
+ builder -> simpleSplitProvider(builder, feature1Path, temp, FeatureClass.class))
+ .addFeatureSplit(
+ builder -> simpleSplitProvider(builder, feature2Path, temp, FeatureClass2.class))
+ .addKeepAllClassesRule()
+ .compile()
+ .writeToZip(basePath);
+
+ CodeInspector baseInspector = new CodeInspector(basePath);
+ assertTrue(baseInspector.clazz(BaseClass.class).isPresent());
+
+ CodeInspector feature1Inspector = new CodeInspector(feature1Path);
+ assertEquals(feature1Inspector.allClasses().size(), 1);
+ assertTrue(feature1Inspector.clazz(FeatureClass.class).isPresent());
+
+ CodeInspector feature2Inspector = new CodeInspector(feature2Path);
+ assertEquals(feature2Inspector.allClasses().size(), 1);
+ assertTrue(feature2Inspector.clazz(FeatureClass2.class).isPresent());
+
+ // Sanity check, we can't call the Feature from the base directly.
+ ProcessResult result =
+ runFeatureOnArt(BaseClass.class, basePath, feature1Path, parameters.getRuntime());
+ assertTrue(result.exitCode != 0);
+
+ result = runFeatureOnArt(FeatureClass.class, basePath, feature1Path, parameters.getRuntime());
+ assertEquals(result.exitCode, 0);
+ assertEquals(result.stdout, StringUtils.lines("Testing base", "Testing feature"));
+
+ result = runFeatureOnArt(FeatureClass2.class, basePath, feature2Path, parameters.getRuntime());
+ assertEquals(result.exitCode, 0);
+ assertEquals(result.stdout, StringUtils.lines("Testing second"));
+ }
+
public static class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello world");
}
}
+
+ public static class BaseClass implements RunInterface {
+ @Override
+ public void run() {
+ new FeatureClass().test();
+ }
+
+ public void test() {
+ System.out.println("Testing base");
+ }
+ }
+
+ public static class FeatureClass implements RunInterface {
+ public void test() {
+ System.out.println("Testing feature");
+ }
+
+ @Override
+ public void run() {
+ new BaseClass().test();
+ test();
+ }
+ }
+
+ public static class FeatureClass2 implements RunInterface {
+ public void test() {
+ System.out.println("Testing second");
+ }
+
+ @Override
+ public void run() {
+ test();
+ }
+ }
}
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 05b8a84..b56827b 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
@@ -1,26 +1,118 @@
package com.android.tools.r8.dexsplitter;
+import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue;
+import com.android.tools.r8.ArchiveProgramResourceProvider;
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.FeatureSplit;
import com.android.tools.r8.R8FullTestBuilder;
import com.android.tools.r8.R8TestCompileResult;
import com.android.tools.r8.TestBase;
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.DexSplitter.Options;
+import com.android.tools.r8.utils.DescriptorUtils;
import com.google.common.collect.ImmutableList;
import dalvik.system.PathClassLoader;
+import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import org.junit.rules.TemporaryFolder;
public class SplitterTestBase extends TestBase {
+ protected static FeatureSplit simpleSplitProvider(
+ FeatureSplit.Builder builder, Path outputPath, TemporaryFolder temp, Class... classes) {
+ return simpleSplitProvider(builder, outputPath, temp, Arrays.asList(classes));
+ }
+
+ protected static FeatureSplit simpleSplitProvider(
+ FeatureSplit.Builder builder,
+ Path outputPath,
+ TemporaryFolder temp,
+ Collection<Class<?>> classes) {
+ List<String> classNames = classes.stream().map(Class::getName).collect(Collectors.toList());
+ Path featureJar;
+ try {
+ featureJar = temp.newFile().toPath();
+ writeClassesToJar(featureJar, classes);
+ } catch (IOException e) {
+ assertTrue(false);
+ return null;
+ }
+
+ builder
+ .addProgramResourceProvider(ArchiveProgramResourceProvider.fromArchive(featureJar))
+ .setProgramConsumer(
+ new ArchiveConsumer(outputPath) {
+ @Override
+ public void accept(
+ int fileIndex,
+ ByteDataView data,
+ Set<String> descriptors,
+ DiagnosticsHandler handler) {
+ assertEquals(classNames.size(), descriptors.size());
+ for (String descriptor : descriptors) {
+ assertTrue(classNames.contains(DescriptorUtils.descriptorToJavaType(descriptor)));
+ }
+ super.accept(fileIndex, data, descriptors, handler);
+ }
+ });
+ return builder.build();
+ }
+
+ protected ProcessResult testR8Splitter(
+ TestParameters parameters,
+ Set<Class<?>> baseClasses,
+ Set<Class<?>> featureClasses,
+ Class toRun,
+ String expectedOutput,
+ Predicate<R8TestCompileResult> predicate,
+ Consumer<R8FullTestBuilder> r8TestConfigurator)
+ throws IOException, CompilationFailedException {
+ Path featureOutput = temp.newFile("feature.zip").toPath();
+
+ R8FullTestBuilder r8FullTestBuilder = testForR8(parameters.getBackend());
+ if (parameters.isCfRuntime()) {
+ // Compiling to jar we need to support the same way of loading code at runtime as
+ // android supports.
+ r8FullTestBuilder
+ .addProgramClasses(PathClassLoader.class)
+ .addKeepClassAndMembersRules(PathClassLoader.class);
+ }
+
+ r8FullTestBuilder
+ .addProgramClasses(SplitRunner.class, RunInterface.class)
+ .addProgramClasses(baseClasses)
+ .addFeatureSplit(
+ builder -> simpleSplitProvider(builder, featureOutput, temp, featureClasses))
+ .setMinApi(parameters.getRuntime())
+ .addKeepMainRule(SplitRunner.class)
+ .addKeepClassRules(toRun);
+
+ r8TestConfigurator.accept(r8FullTestBuilder);
+
+ R8TestCompileResult r8TestCompileResult = r8FullTestBuilder.compile();
+ assertTrue(predicate.test(r8TestCompileResult));
+ Path baseOutput = r8TestCompileResult.writeToZip();
+
+ return runFeatureOnArt(toRun, baseOutput, featureOutput, parameters.getRuntime());
+ }
+
// Compile the passed in classes plus RunInterface and SplitRunner using R8, then split
// based on the base/feature sets. toRun must implement the BaseRunInterface
protected ProcessResult testDexSplitter(
@@ -106,7 +198,15 @@
options.addInputArchive(fullFiles.toString());
DexSplitter.run(options);
- ArtCommandBuilder commandBuilder = new ArtCommandBuilder();
+ return runFeatureOnArt(
+ toRun, splitterBaseDexFile, splitterFeatureDexFile, parameters.getRuntime());
+ }
+
+ protected ProcessResult runFeatureOnArt(
+ Class toRun, Path splitterBaseDexFile, Path splitterFeatureDexFile, TestRuntime runtime)
+ throws IOException {
+ assertTrue(runtime.isDex());
+ ArtCommandBuilder commandBuilder = new ArtCommandBuilder(runtime.asDex().getVm());
commandBuilder.appendClasspath(splitterBaseDexFile.toString());
commandBuilder.appendProgramArgument(toRun.getName());
commandBuilder.appendProgramArgument(splitterFeatureDexFile.toString());