Support feature split resource input/output
This adds consumers and providers for resoruces to the feature splits
The input resources for each split is shrunken and output to the corresponding consumer
The analysis (like the analysis for code) is global, i.e., code references to resource entries are collected for base and all features - even for feature resources
Bug: 287398085
Change-Id: Ic42e843a8ac39285f9bfcb4df9210b50640c3b3c
diff --git a/src/main/java/com/android/tools/r8/FeatureSplit.java b/src/main/java/com/android/tools/r8/FeatureSplit.java
index 97e6cb2..08b1657 100644
--- a/src/main/java/com/android/tools/r8/FeatureSplit.java
+++ b/src/main/java/com/android/tools/r8/FeatureSplit.java
@@ -32,7 +32,7 @@
public class FeatureSplit {
public static final FeatureSplit BASE =
- new FeatureSplit(null, null) {
+ new FeatureSplit(null, null, null, null) {
@Override
public boolean isBase() {
return true;
@@ -40,7 +40,7 @@
};
public static final FeatureSplit BASE_STARTUP =
- new FeatureSplit(null, null) {
+ new FeatureSplit(null, null, null, null) {
@Override
public boolean isBase() {
return true;
@@ -52,13 +52,20 @@
}
};
- private final ProgramConsumer programConsumer;
+ private ProgramConsumer programConsumer;
private final List<ProgramResourceProvider> programResourceProviders;
+ private final AndroidResourceProvider androidResourceProvider;
+ private final AndroidResourceConsumer androidResourceConsumer;
private FeatureSplit(
- ProgramConsumer programConsumer, List<ProgramResourceProvider> programResourceProviders) {
+ ProgramConsumer programConsumer,
+ List<ProgramResourceProvider> programResourceProviders,
+ AndroidResourceProvider androidResourceProvider,
+ AndroidResourceConsumer androidResourceConsumer) {
this.programConsumer = programConsumer;
this.programResourceProviders = programResourceProviders;
+ this.androidResourceProvider = androidResourceProvider;
+ this.androidResourceConsumer = androidResourceConsumer;
}
public boolean isBase() {
@@ -69,6 +76,10 @@
return false;
}
+ void internalSetProgramConsumer(ProgramConsumer consumer) {
+ this.programConsumer = consumer;
+ }
+
public List<ProgramResourceProvider> getProgramResourceProviders() {
return programResourceProviders;
}
@@ -81,6 +92,14 @@
return new Builder(handler);
}
+ public AndroidResourceProvider getAndroidResourceProvider() {
+ return androidResourceProvider;
+ }
+
+ public AndroidResourceConsumer getAndroidResourceConsumer() {
+ return androidResourceConsumer;
+ }
+
/**
* Builder for constructing a FeatureSplit.
*
@@ -90,10 +109,13 @@
public static class Builder {
private ProgramConsumer programConsumer;
private final List<ProgramResourceProvider> programResourceProviders = new ArrayList<>();
+ private AndroidResourceProvider androidResourceProvider;
+ private AndroidResourceConsumer androidResourceConsumer;
@SuppressWarnings("UnusedVariable")
private final DiagnosticsHandler handler;
+
private Builder(DiagnosticsHandler handler) {
this.handler = handler;
}
@@ -121,9 +143,23 @@
return this;
}
+ public Builder setAndroidResourceProvider(AndroidResourceProvider androidResourceProvider) {
+ this.androidResourceProvider = androidResourceProvider;
+ return this;
+ }
+
+ public Builder setAndroidResourceConsumer(AndroidResourceConsumer androidResourceConsumer) {
+ this.androidResourceConsumer = androidResourceConsumer;
+ return this;
+ }
+
/** Build and return the {@link FeatureSplit} */
public FeatureSplit build() {
- return new FeatureSplit(programConsumer, programResourceProviders);
+ return new FeatureSplit(
+ programConsumer,
+ programResourceProviders,
+ androidResourceProvider,
+ androidResourceConsumer);
}
}
}
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index d0264fc..db64a2f 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -114,7 +114,6 @@
import com.android.tools.r8.utils.ExceptionDiagnostic;
import com.android.tools.r8.utils.ExceptionUtils;
import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.Pair;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.SelfRetraceTest;
import com.android.tools.r8.utils.StringDiagnostic;
@@ -131,7 +130,9 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.function.Supplier;
@@ -854,20 +855,21 @@
new DesugaredLibraryKeepRuleGenerator(appView).runIfNecessary(timing);
- List<Pair<Integer, byte[]>> dexFileContent = new ArrayList<>();
+ Map<String, byte[]> dexFileContent = new ConcurrentHashMap<>();
if (options.androidResourceProvider != null && options.androidResourceConsumer != null) {
options.programConsumer =
- new ForwardingConsumer((DexIndexedConsumer) options.programConsumer) {
- @Override
- public void accept(
- int fileIndex,
- ByteDataView data,
- Set<String> descriptors,
- DiagnosticsHandler handler) {
- dexFileContent.add(new Pair<>(fileIndex, data.copyByteData()));
- super.accept(fileIndex, data, descriptors, handler);
- }
- };
+ wrapConsumerStoreBytesInList(
+ dexFileContent, (DexIndexedConsumer) options.programConsumer, "base");
+ if (options.featureSplitConfiguration != null) {
+ int featureIndex = 0;
+ for (FeatureSplit featureSplit : options.featureSplitConfiguration.getFeatureSplits()) {
+ featureSplit.internalSetProgramConsumer(
+ wrapConsumerStoreBytesInList(
+ dexFileContent,
+ (DexIndexedConsumer) featureSplit.getProgramConsumer(),
+ "feature" + featureIndex));
+ }
+ }
}
assert appView.verifyMovedMethodsHaveOriginalMethodPosition();
@@ -894,70 +896,135 @@
}
}
- private void shrinkResources(List<Pair<Integer, byte[]>> dexFileContent) {
+ private static ForwardingConsumer wrapConsumerStoreBytesInList(
+ Map<String, byte[]> dexFileContent,
+ DexIndexedConsumer programConsumer,
+ String classesPrefix) {
+
+ return new ForwardingConsumer(programConsumer) {
+ @Override
+ public void accept(
+ int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) {
+ dexFileContent.put(classesPrefix + "_classes" + fileIndex + ".dex", data.copyByteData());
+ super.accept(fileIndex, data, descriptors, handler);
+ }
+ };
+ }
+
+ private void shrinkResources(Map<String, byte[]> dexFileContent) {
LegacyResourceShrinker.Builder resourceShrinkerBuilder = LegacyResourceShrinker.builder();
Reporter reporter = options.reporter;
- dexFileContent.forEach(p -> resourceShrinkerBuilder.addDexInput(p.getFirst(), p.getSecond()));
+ dexFileContent.forEach(resourceShrinkerBuilder::addDexInput);
try {
- Collection<AndroidResourceInput> androidResources =
- options.androidResourceProvider.getAndroidResources();
- for (AndroidResourceInput androidResource : androidResources) {
- try {
- byte[] bytes = androidResource.getByteStream().readAllBytes();
- Path path = Paths.get(androidResource.getPath().location());
- switch (androidResource.getKind()) {
- case MANIFEST:
- resourceShrinkerBuilder.setManifest(path, bytes);
- break;
- case RES_FOLDER_FILE:
- resourceShrinkerBuilder.addResFolderInput(path, bytes);
- break;
- case RESOURCE_TABLE:
- resourceShrinkerBuilder.setResourceTable(path, bytes);
- break;
- case XML_FILE:
- resourceShrinkerBuilder.addXmlInput(path, bytes);
- break;
- case UNKNOWN:
- break;
+ addResourcesToBuilder(
+ resourceShrinkerBuilder, reporter, options.androidResourceProvider, null);
+ if (options.featureSplitConfiguration != null) {
+ for (FeatureSplit featureSplit : options.featureSplitConfiguration.getFeatureSplits()) {
+ if (featureSplit.getAndroidResourceProvider() != null) {
+ addResourcesToBuilder(
+ resourceShrinkerBuilder,
+ reporter,
+ featureSplit.getAndroidResourceProvider(),
+ featureSplit);
}
- } catch (IOException e) {
- reporter.error(new ExceptionDiagnostic(e, androidResource.getOrigin()));
}
}
LegacyResourceShrinker shrinker = resourceShrinkerBuilder.build();
ShrinkerResult shrinkerResult = shrinker.run();
- AndroidResourceConsumer androidResourceConsumer = options.androidResourceConsumer;
Set<String> toKeep = shrinkerResult.getResFolderEntriesToKeep();
- for (AndroidResourceInput androidResource : androidResources) {
- switch (androidResource.getKind()) {
- case MANIFEST:
- case UNKNOWN:
- androidResourceConsumer.accept(
- new R8PassThroughAndroidResource(androidResource, reporter), reporter);
- break;
- case RESOURCE_TABLE:
- androidResourceConsumer.accept(
- new R8AndroidResourceWithData(
- androidResource, reporter, shrinkerResult.getResourceTableInProtoFormat()),
- reporter);
- break;
- case RES_FOLDER_FILE:
- case XML_FILE:
- if (toKeep.contains(androidResource.getPath().location())) {
- androidResourceConsumer.accept(
- new R8PassThroughAndroidResource(androidResource, reporter), reporter);
- }
- break;
+ writeResourcesToConsumer(
+ reporter,
+ shrinkerResult,
+ toKeep,
+ options.androidResourceProvider,
+ options.androidResourceConsumer,
+ null);
+ if (options.featureSplitConfiguration != null) {
+ for (FeatureSplit featureSplit : options.featureSplitConfiguration.getFeatureSplits()) {
+ if (featureSplit.getAndroidResourceProvider() != null) {
+ writeResourcesToConsumer(
+ reporter,
+ shrinkerResult,
+ toKeep,
+ featureSplit.getAndroidResourceProvider(),
+ featureSplit.getAndroidResourceConsumer(),
+ featureSplit);
+ }
}
}
- androidResourceConsumer.finished(reporter);
} catch (ParserConfigurationException | SAXException | ResourceException | IOException e) {
reporter.error(new ExceptionDiagnostic(e));
}
}
+ private static void writeResourcesToConsumer(
+ Reporter reporter,
+ ShrinkerResult shrinkerResult,
+ Set<String> toKeep,
+ AndroidResourceProvider androidResourceProvider,
+ AndroidResourceConsumer androidResourceConsumer,
+ FeatureSplit featureSplit)
+ throws ResourceException {
+ for (AndroidResourceInput androidResource : androidResourceProvider.getAndroidResources()) {
+ switch (androidResource.getKind()) {
+ case MANIFEST:
+ case UNKNOWN:
+ androidResourceConsumer.accept(
+ new R8PassThroughAndroidResource(androidResource, reporter), reporter);
+ break;
+ case RESOURCE_TABLE:
+ androidResourceConsumer.accept(
+ new R8AndroidResourceWithData(
+ androidResource,
+ reporter,
+ shrinkerResult.getResourceTableInProtoFormat(featureSplit)),
+ reporter);
+ break;
+ case RES_FOLDER_FILE:
+ case XML_FILE:
+ if (toKeep.contains(androidResource.getPath().location())) {
+ androidResourceConsumer.accept(
+ new R8PassThroughAndroidResource(androidResource, reporter), reporter);
+ }
+ break;
+ }
+ }
+ androidResourceConsumer.finished(reporter);
+ }
+
+ private static void addResourcesToBuilder(
+ LegacyResourceShrinker.Builder resourceShrinkerBuilder,
+ Reporter reporter,
+ AndroidResourceProvider androidResourceProvider,
+ FeatureSplit featureSplit)
+ throws ResourceException {
+ for (AndroidResourceInput androidResource : androidResourceProvider.getAndroidResources()) {
+ try {
+ byte[] bytes = androidResource.getByteStream().readAllBytes();
+ Path path = Paths.get(androidResource.getPath().location());
+ switch (androidResource.getKind()) {
+ case MANIFEST:
+ resourceShrinkerBuilder.addManifest(path, bytes);
+ break;
+ case RES_FOLDER_FILE:
+ resourceShrinkerBuilder.addResFolderInput(path, bytes);
+ break;
+ case RESOURCE_TABLE:
+ resourceShrinkerBuilder.addResourceTable(path, bytes, featureSplit);
+ break;
+ case XML_FILE:
+ resourceShrinkerBuilder.addXmlInput(path, bytes);
+ break;
+ case UNKNOWN:
+ break;
+ }
+ } catch (IOException e) {
+ reporter.error(new ExceptionDiagnostic(e, androidResource.getOrigin()));
+ }
+ }
+ }
+
private static boolean allReferencesAssignedApiLevel(
AppView<? extends AppInfoWithClassHierarchy> appView) {
if (!appView.options().apiModelingOptions().isCheckAllApiReferencesAreSet()) {
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index b4ac86e..7621469 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -566,8 +566,8 @@
}
}
for (FeatureSplit featureSplit : featureSplits) {
- assert featureSplit.getProgramConsumer() instanceof DexIndexedConsumer;
- if (!(getProgramConsumer() instanceof DexIndexedConsumer)) {
+ verifyResourceSplitOrProgramSplit(featureSplit);
+ if (getProgramConsumer() != null && !(getProgramConsumer() instanceof DexIndexedConsumer)) {
reporter.error("R8 does not support class file output when using feature splits");
}
}
@@ -587,6 +587,11 @@
super.validate();
}
+ private static void verifyResourceSplitOrProgramSplit(FeatureSplit featureSplit) {
+ assert featureSplit.getProgramConsumer() instanceof DexIndexedConsumer
+ || featureSplit.getAndroidResourceProvider() != null;
+ }
+
@Override
R8Command makeCommand() {
// If printing versions ignore everything else.
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 763a8f5..bd24f29 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -644,10 +644,12 @@
if (featureSplitConfiguration != null) {
for (FeatureSplit featureSplit : featureSplitConfiguration.getFeatureSplits()) {
ProgramConsumer programConsumer = featureSplit.getProgramConsumer();
- programConsumer.finished(reporter);
- DataResourceConsumer dataResourceConsumer = programConsumer.getDataResourceConsumer();
- if (dataResourceConsumer != null) {
- dataResourceConsumer.finished(reporter);
+ if (programConsumer != null) {
+ programConsumer.finished(reporter);
+ DataResourceConsumer dataResourceConsumer = programConsumer.getDataResourceConsumer();
+ if (dataResourceConsumer != null) {
+ dataResourceConsumer.finished(reporter);
+ }
}
}
}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
index 87158c9..b9b0629 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
@@ -22,9 +22,11 @@
import com.android.build.shrinker.usages.ToolsAttributeUsageRecorderKt;
import com.android.ide.common.resources.ResourcesUtil;
import com.android.ide.common.resources.usage.ResourceStore;
+import com.android.tools.r8.FeatureSplit;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import com.google.protobuf.InvalidProtocolBufferException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -40,39 +42,44 @@
import java.util.Set;
import java.util.stream.Collectors;
import javax.xml.parsers.ParserConfigurationException;
-import org.jetbrains.annotations.NotNull;
import org.xml.sax.SAXException;
public class LegacyResourceShrinker {
- private final Map<Integer, byte[]> dexInputs;
+ private final Map<String, byte[]> dexInputs;
private final List<PathAndBytes> resFolderInputs;
private final List<PathAndBytes> xmlInputs;
- private final PathAndBytes manifest;
- private final PathAndBytes resourceTable;
+ private final List<PathAndBytes> manifest;
+ private final Map<PathAndBytes, FeatureSplit> resourceTables;
public static class Builder {
- private final Map<Integer, byte[]> dexInputs = new HashMap<>();
+ private final Map<String, byte[]> dexInputs = new HashMap<>();
private final List<PathAndBytes> resFolderInputs = new ArrayList<>();
private final List<PathAndBytes> xmlInputs = new ArrayList<>();
- private PathAndBytes manifest;
- private PathAndBytes resourceTable;
+ private final List<PathAndBytes> manifests = new ArrayList<>();
+ private final Map<PathAndBytes, FeatureSplit> resourceTables = new HashMap<>();
private Builder() {}
- public Builder setManifest(Path path, byte[] bytes) {
- this.manifest = new PathAndBytes(bytes, path);
+ public Builder addManifest(Path path, byte[] bytes) {
+ manifests.add(new PathAndBytes(bytes, path));
return this;
}
- public Builder setResourceTable(Path path, byte[] bytes) {
- this.resourceTable = new PathAndBytes(bytes, path);
+ public Builder addResourceTable(Path path, byte[] bytes, FeatureSplit featureSplit) {
+ resourceTables.put(new PathAndBytes(bytes, path), featureSplit);
+ try {
+ ResourceTable resourceTable = ResourceTable.parseFrom(bytes);
+ System.currentTimeMillis();
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
return this;
}
- public Builder addDexInput(int index, byte[] bytes) {
- dexInputs.put(index, bytes);
+ public Builder addDexInput(String classesLocation, byte[] bytes) {
+ dexInputs.put(classesLocation, bytes);
return this;
}
@@ -87,22 +94,22 @@
}
public LegacyResourceShrinker build() {
- assert manifest != null && resourceTable != null;
+ assert manifests != null && resourceTables != null;
return new LegacyResourceShrinker(
- dexInputs, resFolderInputs, manifest, resourceTable, xmlInputs);
+ dexInputs, resFolderInputs, manifests, resourceTables, xmlInputs);
}
}
private LegacyResourceShrinker(
- Map<Integer, byte[]> dexInputs,
+ Map<String, byte[]> dexInputs,
List<PathAndBytes> resFolderInputs,
- PathAndBytes manifest,
- PathAndBytes resourceTable,
+ List<PathAndBytes> manifests,
+ Map<PathAndBytes, FeatureSplit> resourceTables,
List<PathAndBytes> xmlInputs) {
this.dexInputs = dexInputs;
this.resFolderInputs = resFolderInputs;
- this.manifest = manifest;
- this.resourceTable = resourceTable;
+ this.manifest = manifests;
+ this.resourceTables = resourceTables;
this.xmlInputs = xmlInputs;
}
@@ -111,41 +118,48 @@
}
public ShrinkerResult run() throws IOException, ParserConfigurationException, SAXException {
- R8ResourceShrinkerModel model = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, false);
- ResourceTable loadedResourceTable = ResourceTable.parseFrom(resourceTable.bytes);
- model.instantiateFromResourceTable(loadedResourceTable);
- for (Entry<Integer, byte[]> entry : dexInputs.entrySet()) {
+ R8ResourceShrinkerModel model = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, true);
+ for (PathAndBytes pathAndBytes : resourceTables.keySet()) {
+ ResourceTable loadedResourceTable = ResourceTable.parseFrom(pathAndBytes.bytes);
+ model.instantiateFromResourceTable(loadedResourceTable);
+ }
+ for (Entry<String, byte[]> entry : dexInputs.entrySet()) {
// The analysis needs an origin for the dex files, synthesize an easy recognizable one.
- Path inMemoryR8 = Paths.get("in_memory_r8_classes" + entry.getKey() + ".dex");
+ Path inMemoryR8 = Paths.get("in_memory_r8_" + entry.getKey() + ".dex");
R8ResourceShrinker.runResourceShrinkerAnalysis(
entry.getValue(), inMemoryR8, new DexFileAnalysisCallback(inMemoryR8, model));
}
- ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode(
- XmlNode.parseFrom(manifest.bytes), model);
+ for (PathAndBytes pathAndBytes : manifest) {
+ ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode(
+ XmlNode.parseFrom(pathAndBytes.bytes), model);
+ }
for (PathAndBytes xmlInput : xmlInputs) {
if (xmlInput.path.startsWith("res/raw")) {
ToolsAttributeUsageRecorderKt.processRawXml(getUtfReader(xmlInput.getBytes()), model);
}
}
- new ProtoResourcesGraphBuilder(
- new ResFolderFileTree() {
- Map<String, PathAndBytes> pathToBytes =
- new ImmutableMap.Builder<String, PathAndBytes>()
- .putAll(
- xmlInputs.stream()
- .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
- .putAll(
- resFolderInputs.stream()
- .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
- .build();
- @Override
- public byte[] getEntryByName(@NotNull String pathInRes) {
- return pathToBytes.get(pathInRes).getBytes();
- }
- },
- unused -> loadedResourceTable)
- .buildGraph(model);
+ ImmutableMap<String, PathAndBytes> resFolderMappings =
+ new ImmutableMap.Builder<String, PathAndBytes>()
+ .putAll(
+ xmlInputs.stream()
+ .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
+ .putAll(
+ resFolderInputs.stream()
+ .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
+ .build();
+ for (PathAndBytes pathAndBytes : resourceTables.keySet()) {
+ ResourceTable resourceTable = ResourceTable.parseFrom(pathAndBytes.bytes);
+ new ProtoResourcesGraphBuilder(
+ new ResFolderFileTree() {
+ @Override
+ public byte[] getEntryByName(String pathInRes) {
+ return resFolderMappings.get(pathInRes).getBytes();
+ }
+ },
+ unused -> resourceTable)
+ .buildGraph(model);
+ }
ResourceStore resourceStore = model.getResourceStore();
resourceStore.processToolsAttributes();
model.keepPossiblyReferencedResources();
@@ -164,9 +178,14 @@
.filter(r -> !r.isReachable())
.map(r -> r.value)
.collect(Collectors.toList());
- ResourceTable shrunkenResourceTable =
- ResourceTableUtilKt.nullOutEntriesWithIds(loadedResourceTable, resourceIdsToRemove);
- return new ShrinkerResult(resEntriesToKeep.build(), shrunkenResourceTable.toByteArray());
+ Map<FeatureSplit, ResourceTable> shrunkenTables = new HashMap<>();
+ for (Entry<PathAndBytes, FeatureSplit> entry : resourceTables.entrySet()) {
+ ResourceTable shrunkenResourceTable =
+ ResourceTableUtilKt.nullOutEntriesWithIds(
+ ResourceTable.parseFrom(entry.getKey().bytes), resourceIdsToRemove);
+ shrunkenTables.put(entry.getValue(), shrunkenResourceTable);
+ }
+ return new ShrinkerResult(resEntriesToKeep.build(), shrunkenTables);
}
// Lifted from com/android/utils/XmlUtils.java which we can't easily update internal dependency
@@ -236,15 +255,17 @@
public static class ShrinkerResult {
private final Set<String> resFolderEntriesToKeep;
- private final byte[] resourceTableInProtoFormat;
+ private final Map<FeatureSplit, ResourceTable> resourceTableInProtoFormat;
- public ShrinkerResult(Set<String> resFolderEntriesToKeep, byte[] resourceTableInProtoFormat) {
+ public ShrinkerResult(
+ Set<String> resFolderEntriesToKeep,
+ Map<FeatureSplit, ResourceTable> resourceTableInProtoFormat) {
this.resFolderEntriesToKeep = resFolderEntriesToKeep;
this.resourceTableInProtoFormat = resourceTableInProtoFormat;
}
- public byte[] getResourceTableInProtoFormat() {
- return resourceTableInProtoFormat;
+ public byte[] getResourceTableInProtoFormat(FeatureSplit featureSplit) {
+ return resourceTableInProtoFormat.get(featureSplit).toByteArray();
}
public Set<String> getResFolderEntriesToKeep() {
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index b24f608..7ae3463 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -51,6 +51,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -83,6 +84,7 @@
private final List<String> applyMappingMaps = new ArrayList<>();
private final List<Path> features = new ArrayList<>();
private Path resourceShrinkerOutput = null;
+ private HashMap<String, Path> resourceShrinkerOutputForFeatures = new HashMap<>();
private PartitionMapConsumer partitionMapConsumer = null;
@Override
@@ -170,7 +172,8 @@
getMinApiLevel(),
features,
residualArtProfiles,
- resourceShrinkerOutput);
+ resourceShrinkerOutput,
+ resourceShrinkerOutputForFeatures);
switch (allowedDiagnosticMessages) {
case ALL:
compileResult.getDiagnosticMessages().assertAllDiagnosticsMatch(new IsAnything<>());
@@ -878,19 +881,33 @@
testResource, getState().getNewTempFile("resourceshrinkeroutput.zip"));
}
- public T addAndroidResources(AndroidTestResource testResource, Path output) throws IOException {
- addResourceShrinkerProviderAndConsumer(testResource.getResourceZip(), output);
+ public T addFeatureSplitAndroidResources(AndroidTestResource testResource, String featureName)
+ throws IOException {
+ Path outputFile = getState().getNewTempFile("resourceshrinkeroutput_" + featureName + ".zip");
+ resourceShrinkerOutputForFeatures.put(featureName, outputFile);
+ getBuilder()
+ .addFeatureSplit(
+ featureSplitGenerator -> {
+ featureSplitGenerator.setAndroidResourceConsumer(
+ new ArchiveProtoAndroidResourceConsumer(outputFile));
+ Path resourceZip = testResource.getResourceZip();
+ featureSplitGenerator.setAndroidResourceProvider(
+ new ArchiveProtoAndroidResourceProvider(
+ resourceZip, new PathOrigin(resourceZip)));
+ return featureSplitGenerator.build();
+ });
return addProgramClassFileData(testResource.getRClass().getClassFileData());
}
- private T addResourceShrinkerProviderAndConsumer(Path resources, Path output) throws IOException {
+ public T addAndroidResources(AndroidTestResource testResource, Path output) throws IOException {
+ Path resources = testResource.getResourceZip();
resourceShrinkerOutput = output;
getBuilder()
.setAndroidResourceProvider(
new ArchiveProtoAndroidResourceProvider(resources, new PathOrigin(resources)));
- getBuilder()
- .setAndroidResourceConsumer(
- new ArchiveProtoAndroidResourceConsumer(resourceShrinkerOutput));
- return self();
+ getBuilder().setAndroidResourceConsumer(new ArchiveProtoAndroidResourceConsumer(output));
+ self();
+ return addProgramClassFileData(testResource.getRClass().getClassFileData());
}
+
}
diff --git a/src/test/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
index 82997e9..20b9f2d 100644
--- a/src/test/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
@@ -27,7 +27,9 @@
import com.android.tools.r8.utils.graphinspector.GraphInspector;
import java.io.IOException;
import java.nio.file.Path;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
@@ -40,6 +42,7 @@
private final List<Path> features;
private final List<ExternalArtProfile> residualArtProfiles;
private final Path resourceShrinkerOutput;
+ private final Map<String, Path> resourceShrinkerOutputForFeatures;
R8TestCompileResult(
TestState state,
@@ -53,7 +56,8 @@
int minApiLevel,
List<Path> features,
List<ExternalArtProfile> residualArtProfiles,
- Path resourceShrinkerOutput) {
+ Path resourceShrinkerOutput,
+ HashMap<String, Path> resourceShrinkerOutputForFeatures) {
super(state, app, minApiLevel, outputMode, libraryDesugaringTestConfiguration);
this.proguardConfiguration = proguardConfiguration;
this.syntheticProguardRules = syntheticProguardRules;
@@ -62,6 +66,7 @@
this.features = features;
this.residualArtProfiles = residualArtProfiles;
this.resourceShrinkerOutput = resourceShrinkerOutput;
+ this.resourceShrinkerOutputForFeatures = resourceShrinkerOutputForFeatures;
}
@Override
@@ -164,6 +169,14 @@
return self();
}
+ public <E extends Throwable> R8TestCompileResult inspectShrunkenResourcesForFeature(
+ Consumer<ResourceTableInspector> consumer, String featureName) throws IOException {
+ Path path = resourceShrinkerOutputForFeatures.get(featureName);
+ assertNotNull(path);
+ consumer.accept(new ResourceTableInspector(ZipUtils.readSingleEntry(path, "resources.pb")));
+ return self();
+ }
+
public GraphInspector graphInspector() throws IOException {
assert graphConsumer != null;
return new GraphInspector(graphConsumer, inspector());
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
index 63de716..a54f090 100644
--- a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -18,6 +18,7 @@
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.StreamUtils;
+import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.ZipUtils;
import com.google.common.collect.MoreCollectors;
import com.google.protobuf.InvalidProtocolBufferException;
@@ -27,6 +28,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -141,6 +143,10 @@
return mapping.containsKey(type) && mapping.get(type).containsValueFor(name);
}
+ public Collection<String> entriesForType(String type) {
+ return mapping.get(type).mapping.keySet();
+ }
+
public static class ResourceNameToValueMapping {
private final Map<String, List<ResourceValue>> mapping = new HashMap<>();
@@ -198,11 +204,17 @@
}
public void assertContainsResourceWithName(String type, String name) {
- Assert.assertTrue(testResourceTable.containsValueFor(type, name));
+ Assert.assertTrue(
+ StringUtils.join(",", entries(type)), testResourceTable.containsValueFor(type, name));
}
public void assertDoesNotContainResourceWithName(String type, String name) {
- Assert.assertFalse(testResourceTable.containsValueFor(type, name));
+ Assert.assertFalse(
+ StringUtils.join(",", entries(type)), testResourceTable.containsValueFor(type, name));
+ }
+
+ public Collection<String> entries(String type) {
+ return testResourceTable.entriesForType(type);
}
}
@@ -212,6 +224,7 @@
private final Map<String, byte[]> drawables = new TreeMap<>();
private final Map<String, String> xmlFiles = new TreeMap<>();
private final List<Class<?>> classesToRemap = new ArrayList<>();
+ private int packageId = 0x7f;
// Create the android resources from the passed in R classes
// All values will be generated based on the fields in the class.
@@ -257,6 +270,11 @@
return this;
}
+ AndroidTestResourceBuilder setPackageId(int packageId) {
+ this.packageId = packageId;
+ return this;
+ }
+
AndroidTestResourceBuilder addDrawable(String name, byte[] value) {
drawables.put(name, value);
return this;
@@ -288,7 +306,7 @@
Path output = temp.newFile("resources.zip").toPath();
Path rClassOutputDir = temp.newFolder("aapt_R_class").toPath();
- compileWithAapt2(resFolder, manifestPath, rClassOutputDir, output, temp);
+ compileWithAapt2(resFolder, manifestPath, rClassOutputDir, output, temp, packageId);
Path rClassJavaFile =
Files.walk(rClassOutputDir)
.filter(path -> path.endsWith("R.java"))
@@ -367,7 +385,12 @@
}
public static void compileWithAapt2(
- Path resFolder, Path manifest, Path rClassFolder, Path resourceZip, TemporaryFolder temp)
+ Path resFolder,
+ Path manifest,
+ Path rClassFolder,
+ Path resourceZip,
+ TemporaryFolder temp,
+ int packageId)
throws IOException {
Path compileOutput = temp.newFile("compiled.zip").toPath();
ProcessResult compileProcessResult =
@@ -386,6 +409,11 @@
rClassFolder.toString(),
"--manifest",
manifest.toString(),
+ "--package-id",
+ "" + packageId,
+ "--allow-reserved-package-id",
+ "--rename-resources-package",
+ "thepackage" + packageId + ".foobar",
"--proto-format",
compileOutput.toString());
failOnError(linkProcesResult);
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java
new file mode 100644
index 0000000..2b257b4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java
@@ -0,0 +1,152 @@
+// Copyright (c) 2023, 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.androidresources;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticException;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.CompilationFailedException;
+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.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.androidresources.ResourceShrinkingWithFeatures.FeatureSplit.FeatureSplitMain;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ResourceShrinkingWithFeatures extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection parameters() {
+ return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+ }
+
+ public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+ return new AndroidTestResourceBuilder()
+ .withSimpleManifestAndAppNameString()
+ .addRClassInitializeWithDefaultValues(R.string.class)
+ .build(temp);
+ }
+
+ public static AndroidTestResource getFeatureSplitTestResources(TemporaryFolder temp)
+ throws IOException {
+ return new AndroidTestResourceBuilder()
+ .withSimpleManifestAndAppNameString()
+ .setPackageId(0x7E)
+ .addRClassInitializeWithDefaultValues(FeatureSplit.R.string.class)
+ .build(temp);
+ }
+
+ @Test
+ public void testFailureIfNotResourcesOrCode() throws Exception {
+ try {
+ testForR8(parameters.getBackend())
+ .setMinApi(parameters)
+ .addProgramClasses(Base.class)
+ .addFeatureSplit(builder -> builder.build())
+ .compileWithExpectedDiagnostics(
+ diagnostics -> {
+ diagnostics.assertErrorThatMatches(diagnosticException(AssertionError.class));
+ });
+ } catch (CompilationFailedException e) {
+ return;
+ }
+ fail();
+ }
+
+ @Test
+ public void testR8ReferenceFeatureResourcesFromFeature() throws Exception {
+ // We reference a feature resource from a feature.
+ testR8(false);
+ }
+
+ @Test
+ public void testR8ReferenceFeatureResourcesFromBase() throws Exception {
+ // We reference a feature resource from the base.
+ testR8(true);
+ }
+
+ private void testR8(boolean referenceFromBase) throws Exception {
+ TemporaryFolder featureSplitTemp = ToolHelper.getTemporaryFolderForTest();
+ featureSplitTemp.create();
+ R8FullTestBuilder r8FullTestBuilder =
+ testForR8(parameters.getBackend()).setMinApi(parameters).addProgramClasses(Base.class);
+ if (referenceFromBase) {
+ r8FullTestBuilder.addProgramClasses(FeatureSplit.FeatureSplitMain.class);
+ } else {
+ r8FullTestBuilder.addFeatureSplit(FeatureSplit.FeatureSplitMain.class);
+ }
+ R8TestCompileResult compile =
+ r8FullTestBuilder
+ .addAndroidResources(getTestResources(temp))
+ .addFeatureSplitAndroidResources(
+ getFeatureSplitTestResources(featureSplitTemp), FeatureSplit.class.getName())
+ .addKeepMainRule(Base.class)
+ .addKeepMainRule(FeatureSplitMain.class)
+ .compile();
+ compile
+ .inspectShrunkenResources(
+ resourceTableInspector -> {
+ resourceTableInspector.assertContainsResourceWithName("string", "base_used");
+ resourceTableInspector.assertDoesNotContainResourceWithName("string", "base_unused");
+ })
+ .inspectShrunkenResourcesForFeature(
+ resourceTableInspector -> {
+ resourceTableInspector.assertContainsResourceWithName("string", "feature_used");
+ resourceTableInspector.assertDoesNotContainResourceWithName(
+ "string", "feature_unused");
+ },
+ FeatureSplit.class.getName())
+ .run(parameters.getRuntime(), Base.class)
+ .assertSuccess();
+ }
+
+ public static class Base {
+
+ public static void main(String[] args) {
+ if (System.currentTimeMillis() == 0) {
+ System.out.println(R.string.base_used);
+ }
+ }
+ }
+
+ public static class R {
+
+ public static class string {
+
+ public static int base_used;
+ public static int base_unused;
+ }
+ }
+
+ public static class FeatureSplit {
+ public static class FeatureSplitMain {
+ public static void main(String[] args) {
+ if (System.currentTimeMillis() == 0) {
+ System.out.println(R.string.feature_used);
+ }
+ }
+ }
+
+ public static class R {
+ public static class string {
+ public static int feature_used;
+ public static int feature_unused;
+ }
+ }
+ }
+}