blob: c3e2ad4852662d4c2ce4d72e60e31979a0136af5 [file] [log] [blame]
// 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.build.shrinker.r8integration;
import static com.android.build.shrinker.r8integration.LegacyResourceShrinker.getUtfReader;
import com.android.aapt.Resources;
import com.android.aapt.Resources.ConfigValue;
import com.android.aapt.Resources.Entry;
import com.android.aapt.Resources.FileReference;
import com.android.aapt.Resources.Item;
import com.android.aapt.Resources.Package;
import com.android.aapt.Resources.ResourceTable;
import com.android.aapt.Resources.Value;
import com.android.aapt.Resources.XmlAttribute;
import com.android.aapt.Resources.XmlElement;
import com.android.aapt.Resources.XmlNode;
import com.android.build.shrinker.NoDebugReporter;
import com.android.build.shrinker.ResourceShrinkerImplKt;
import com.android.build.shrinker.ResourceShrinkerModel;
import com.android.build.shrinker.ResourceTableUtilKt;
import com.android.build.shrinker.ShrinkerDebugReporter;
import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder;
import com.android.build.shrinker.r8integration.LegacyResourceShrinker.ShrinkerResult;
import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorderKt;
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.ide.common.resources.usage.ResourceUsageModel;
import com.android.ide.common.resources.usage.ResourceUsageModel.Resource;
import com.android.resources.ResourceType;
import com.android.tools.r8.FeatureSplit;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.origin.PathOrigin;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class R8ResourceShrinkerState {
private final Function<Exception, RuntimeException> errorHandler;
private final R8ResourceShrinkerModel r8ResourceShrinkerModel;
private final Map<String, Supplier<InputStream>> xmlFileProviders = new HashMap<>();
private final List<Supplier<InputStream>> manifestProviders = new ArrayList<>();
private final Map<String, Supplier<InputStream>> resfileProviders = new HashMap<>();
private final Map<ResourceTable, FeatureSplit> resourceTables = new HashMap<>();
private ClassReferenceCallback enqueuerCallback;
private Map<Integer, String> resourceIdToXmlFiles;
private Set<String> packageNames;
private final Set<String> seenNoneClassValues = new HashSet<>();
private final Set<Integer> seenResourceIds = new HashSet<>();
@FunctionalInterface
public interface ClassReferenceCallback {
boolean tryClass(String possibleClass, Origin xmlFileOrigin);
}
public R8ResourceShrinkerState(Function<Exception, RuntimeException> errorHandler) {
r8ResourceShrinkerModel = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, true);
this.errorHandler = errorHandler;
}
public void trace(int id) {
if (!seenResourceIds.add(id)) {
return;
}
Resource resource = r8ResourceShrinkerModel.getResourceStore().getResource(id);
if (resource == null) {
return;
}
ResourceUsageModel.markReachable(resource);
traceXmlForResourceId(id);
if (resource.references != null) {
for (Resource reference : resource.references) {
if (!reference.isReachable()) {
trace(reference.value);
}
}
}
}
public void traceKeepXmlAndManifest() {
// We start by building the root set of all keep/discard rules to find those pinned resources
// before marking additional resources in the trace.
// We then explicitly trace those resources to transitively get the full set of reachable
// resources and code.
try {
updateModelWithKeepXmlReferences();
} catch (IOException e) {
throw errorHandler.apply(e);
}
// ProcessToolsAttribute returns the resources that becomes live
r8ResourceShrinkerModel
.getResourceStore()
.processToolsAttributes()
.forEach(resource -> trace(resource.value));
for (Supplier<InputStream> manifestProvider : manifestProviders) {
traceXml("AndroidManifest.xml", manifestProvider.get());
}
}
public void setEnqueuerCallback(ClassReferenceCallback enqueuerCallback) {
assert this.enqueuerCallback == null;
this.enqueuerCallback = enqueuerCallback;
}
private synchronized Set<String> getPackageNames() {
// TODO(b/325888516): Consider only doing this for the package corresponding to the current
// feature.
if (packageNames == null) {
packageNames = new HashSet<>();
for (ResourceTable resourceTable : resourceTables.keySet()) {
for (Package aPackage : resourceTable.getPackageList()) {
packageNames.add(aPackage.getPackageName());
}
}
}
return packageNames;
}
public void addManifestProvider(Supplier<InputStream> manifestProvider) {
this.manifestProviders.add(manifestProvider);
}
public void addXmlFileProvider(Supplier<InputStream> inputStreamSupplier, String location) {
this.xmlFileProviders.put(location, inputStreamSupplier);
}
public void addResFileProvider(Supplier<InputStream> inputStreamSupplier, String location) {
this.resfileProviders.put(location, inputStreamSupplier);
}
public void addResourceTable(InputStream inputStream, FeatureSplit featureSplit) {
this.resourceTables.put(
r8ResourceShrinkerModel.instantiateFromResourceTable(inputStream, true), featureSplit);
}
public R8ResourceShrinkerModel getR8ResourceShrinkerModel() {
return r8ResourceShrinkerModel;
}
private byte[] getXmlOrResFileBytes(String path) {
assert !path.startsWith("res/");
String pathWithRes = "res/" + path;
Supplier<InputStream> inputStreamSupplier = xmlFileProviders.get(pathWithRes);
if (inputStreamSupplier == null) {
inputStreamSupplier = resfileProviders.get(pathWithRes);
}
if (inputStreamSupplier == null) {
// Ill formed resource table with file references inside res/ that does not exist.
return null;
}
try {
return inputStreamSupplier.get().readAllBytes();
} catch (IOException ex) {
throw errorHandler.apply(ex);
}
}
public void setupReferences() {
for (ResourceTable resourceTable : resourceTables.keySet()) {
new ProtoResourcesGraphBuilder(this::getXmlOrResFileBytes, unused -> resourceTable)
.buildGraph(r8ResourceShrinkerModel);
}
}
public ShrinkerResult shrinkModel() throws IOException {
updateModelWithManifestReferences();
updateModelWithKeepXmlReferences();
ResourceStore resourceStore = r8ResourceShrinkerModel.getResourceStore();
resourceStore.processToolsAttributes();
ImmutableSet<String> resEntriesToKeep = getResEntriesToKeep(resourceStore);
List<Integer> resourceIdsToRemove = getResourcesToRemove();
Map<FeatureSplit, ResourceTable> shrunkenTables = new IdentityHashMap<>();
resourceTables.forEach(
(resourceTable, featureSplit) ->
shrunkenTables.put(
featureSplit,
ResourceTableUtilKt.nullOutEntriesWithIds(
resourceTable, resourceIdsToRemove, true)));
return new ShrinkerResult(resEntriesToKeep, shrunkenTables);
}
private ImmutableSet<String> getResEntriesToKeep(ResourceStore resourceStore) {
ImmutableSet.Builder<String> resEntriesToKeep = new ImmutableSet.Builder<>();
for (String path : Iterables.concat(xmlFileProviders.keySet(), resfileProviders.keySet())) {
if (ResourceShrinkerImplKt.isJarPathReachable(resourceStore, path)) {
resEntriesToKeep.add(path);
}
}
return resEntriesToKeep.build();
}
private void traceXmlForResourceId(int id) {
String xmlFile = getResourceIdToXmlFiles().get(id);
if (xmlFile != null) {
InputStream inputStream = xmlFileProviders.get(xmlFile).get();
traceXml(xmlFile, inputStream);
}
}
private void traceXml(String xmlFile, InputStream inputStream) {
try {
XmlNode xmlNode = XmlNode.parseFrom(inputStream);
visitNode(xmlNode, xmlFile);
} catch (IOException e) {
errorHandler.apply(e);
}
}
private void tryEnqueuerOnString(String possibleClass, String xmlName) {
// There are a lot of xml tags and attributes that are evaluated over and over, if it is
// not a class, ignore it.
if (seenNoneClassValues.contains(possibleClass)) {
return;
}
if (!enqueuerCallback.tryClass(possibleClass, new PathOrigin(Paths.get(xmlName)))) {
seenNoneClassValues.add(possibleClass);
}
}
private void visitNode(XmlNode xmlNode, String xmlName) {
XmlElement element = xmlNode.getElement();
tryEnqueuerOnString(element.getName(), xmlName);
for (XmlAttribute xmlAttribute : element.getAttributeList()) {
String value = xmlAttribute.getValue();
tryEnqueuerOnString(value, xmlName);
if (value.startsWith(".")) {
// package specific names, e.g. context
getPackageNames().forEach(s -> tryEnqueuerOnString(s + value, xmlName));
}
}
element.getChildList().forEach(e -> visitNode(e, xmlName));
}
public Map<Integer, String> getResourceIdToXmlFiles() {
if (resourceIdToXmlFiles == null) {
resourceIdToXmlFiles = new HashMap<>();
for (ResourceTable resourceTable : resourceTables.keySet()) {
for (Package packageEntry : resourceTable.getPackageList()) {
for (Resources.Type type : packageEntry.getTypeList()) {
for (Entry entry : type.getEntryList()) {
for (ConfigValue configValue : entry.getConfigValueList()) {
if (configValue.hasValue()) {
Value value = configValue.getValue();
if (value.hasItem()) {
Item item = value.getItem();
if (item.hasFile()) {
FileReference file = item.getFile();
if (file.getType() == FileReference.Type.PROTO_XML) {
int id = ResourceTableUtilKt.toIdentifier(packageEntry, type, entry);
resourceIdToXmlFiles.put(id, file.getPath());
}
}
}
}
}
}
}
}
}
}
return resourceIdToXmlFiles;
}
private List<Integer> getResourcesToRemove() {
return r8ResourceShrinkerModel.getResourceStore().getResources().stream()
.filter(r -> !r.isReachable() && !r.isPublic())
.filter(r -> r.type != ResourceType.ID)
.map(r -> r.value)
.collect(Collectors.toList());
}
// Temporary to support updating the reachable entries from the manifest, we need to instead
// trace these in the enqueuer.
public void updateModelWithManifestReferences() throws IOException {
for (Supplier<InputStream> manifestProvider : manifestProviders) {
ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode(
XmlNode.parseFrom(manifestProvider.get()), r8ResourceShrinkerModel);
}
}
public void updateModelWithKeepXmlReferences() throws IOException {
for (Map.Entry<String, Supplier<InputStream>> entry : xmlFileProviders.entrySet()) {
if (entry.getKey().startsWith("res/raw")) {
ToolsAttributeUsageRecorderKt.processRawXml(
getUtfReader(entry.getValue().get().readAllBytes()), r8ResourceShrinkerModel);
}
}
}
public void enqueuerDone(boolean isFinalTreeshaking) {
enqueuerCallback = null;
seenResourceIds.clear();
if (!isFinalTreeshaking) {
// After final tree shaking we will need the reachability bits to decide what to write out
// from the model.
clearReachableBits();
}
}
public void clearReachableBits() {
for (Resource resource : r8ResourceShrinkerModel.getResourceStore().getResources()) {
resource.setReachable(false);
}
}
public static class R8ResourceShrinkerModel extends ResourceShrinkerModel {
private final Map<Integer, String> stringResourcesWithSingleValue = new HashMap<>();
public R8ResourceShrinkerModel(
ShrinkerDebugReporter debugReporter, boolean supportMultipackages) {
super(debugReporter, supportMultipackages);
}
public String getSingleStringValueOrNull(int id) {
return stringResourcesWithSingleValue.get(id);
}
// Similar to instantiation in ProtoResourceTableGatherer, but using an inputstream.
ResourceTable instantiateFromResourceTable(InputStream inputStream, boolean includeStyleables) {
try {
ResourceTable resourceTable = ResourceTable.parseFrom(inputStream);
instantiateFromResourceTable(resourceTable, includeStyleables);
return resourceTable;
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
void instantiateFromResourceTable(ResourceTable resourceTable, boolean includeStyleables) {
ResourceTableUtilKt.entriesSequence(resourceTable)
.iterator()
.forEachRemaining(
entryWrapper -> {
ResourceType resourceType = ResourceType.fromClassName(entryWrapper.getType());
Entry entry = entryWrapper.getEntry();
int entryId = entryWrapper.getId();
recordSingleValueResources(resourceType, entry, entryId);
if (resourceType != ResourceType.STYLEABLE || includeStyleables) {
this.addResource(
resourceType,
entryWrapper.getPackageName(),
ResourcesUtil.resourceNameToFieldName(entry.getName()),
entryId);
}
});
}
private void recordSingleValueResources(ResourceType resourceType, Entry entry, int entryId) {
if (!entry.hasOverlayableItem() && entry.getConfigValueList().size() == 1) {
if (resourceType == ResourceType.STRING) {
ConfigValue configValue = entry.getConfigValue(0);
if (configValue.hasValue()) {
Value value = configValue.getValue();
if (value.hasItem()) {
Item item = value.getItem();
if (item.hasStr()) {
stringResourcesWithSingleValue.put(entryId, item.getStr().getValue());
}
}
}
}
}
}
}
}