Reapply "Initial setup for doing legacy resource shrinking in R8" This reverts commit 8ea20ac4540f3800f13f5f54c52d3456bf9409fe. Bug: 305892375 Bug: 287398085 Change-Id: If6e6ee396cd6dcee4619b733d94f6dd456e4b7b6
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java new file mode 100644 index 0000000..9fd4163 --- /dev/null +++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
@@ -0,0 +1,270 @@ +// 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 java.nio.charset.StandardCharsets.UTF_16BE; +import static java.nio.charset.StandardCharsets.UTF_16LE; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.android.aapt.Resources.ResourceTable; +import com.android.aapt.Resources.XmlNode; +import com.android.build.shrinker.NoDebugReporter; +import com.android.build.shrinker.ResourceShrinkerImplKt; +import com.android.build.shrinker.ResourceTableUtilKt; +import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder; +import com.android.build.shrinker.graph.ResFolderFileTree; +import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel; +import com.android.build.shrinker.usages.DexFileAnalysisCallback; +import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorderKt; +import com.android.build.shrinker.usages.R8ResourceShrinker; +import com.android.build.shrinker.usages.ToolsAttributeUsageRecorderKt; +import com.android.ide.common.resources.usage.ResourceStore; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +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 List<PathAndBytes> resFolderInputs; + private final List<PathAndBytes> xmlInputs; + private final PathAndBytes manifest; + private final PathAndBytes resourceTable; + + public static class Builder { + + private final Map<Integer, 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 Builder() {} + + public Builder setManifest(Path path, byte[] bytes) { + this.manifest = new PathAndBytes(bytes, path); + return this; + } + + public Builder setResourceTable(Path path, byte[] bytes) { + this.resourceTable = new PathAndBytes(bytes, path); + return this; + } + + public Builder addDexInput(int index, byte[] bytes) { + dexInputs.put(index, bytes); + return this; + } + + public Builder addResFolderInput(Path path, byte[] bytes) { + resFolderInputs.add(new PathAndBytes(bytes, path)); + return this; + } + + public Builder addXmlInput(Path path, byte[] bytes) { + xmlInputs.add(new PathAndBytes(bytes, path)); + return this; + } + + public LegacyResourceShrinker build() { + assert manifest != null && resourceTable != null; + return new LegacyResourceShrinker( + dexInputs, resFolderInputs, manifest, resourceTable, xmlInputs); + } + } + + private LegacyResourceShrinker( + Map<Integer, byte[]> dexInputs, + List<PathAndBytes> resFolderInputs, + PathAndBytes manifest, + PathAndBytes resourceTable, + List<PathAndBytes> xmlInputs) { + this.dexInputs = dexInputs; + this.resFolderInputs = resFolderInputs; + this.manifest = manifest; + this.resourceTable = resourceTable; + this.xmlInputs = xmlInputs; + } + + public static Builder builder() { + return new Builder(); + } + + 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()) { + // 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"); + R8ResourceShrinker.runResourceShrinkerAnalysis( + entry.getValue(), inMemoryR8, new DexFileAnalysisCallback(inMemoryR8, model)); + } + ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode( + XmlNode.parseFrom(manifest.bytes), model); + for (PathAndBytes xmlInput : xmlInputs) { + 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); + ResourceStore resourceStore = model.getResourceStore(); + resourceStore.processToolsAttributes(); + model.keepPossiblyReferencedResources(); + ImmutableSet.Builder<String> resEntriesToKeep = new ImmutableSet.Builder<>(); + for (PathAndBytes xmlInput : Iterables.concat(xmlInputs, resFolderInputs)) { + if (ResourceShrinkerImplKt.isJarPathReachable(resourceStore, xmlInput.path.toString())) { + resEntriesToKeep.add(xmlInput.path.toString()); + } + } + List<Integer> resourceIdsToRemove = + model.getResourceStore().getResources().stream() + .filter(r -> !r.isReachable()) + .map(r -> r.value) + .collect(Collectors.toList()); + ResourceTable shrunkenResourceTable = + ResourceTableUtilKt.nullOutEntriesWithIds(loadedResourceTable, resourceIdsToRemove); + return new ShrinkerResult(resEntriesToKeep.build(), shrunkenResourceTable.toByteArray()); + } + + // Lifted from com/android/utils/XmlUtils.java which we can't easily update internal dependency + // for. + /** + * Returns a character reader for the given bytes, which must be a UTF encoded file. + * + * <p>The reader does not need to be closed by the caller (because the file is read in full in one + * shot and the resulting array is then wrapped in a byte array input stream, which does not need + * to be closed.) + */ + public static Reader getUtfReader(byte[] bytes) throws IOException { + int length = bytes.length; + if (length == 0) { + return new StringReader(""); + } + + switch (bytes[0]) { + case (byte) 0xEF: + { + if (length >= 3 && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) { + // UTF-8 BOM: EF BB BF: Skip it + return new InputStreamReader(new ByteArrayInputStream(bytes, 3, length - 3), UTF_8); + } + break; + } + case (byte) 0xFE: + { + if (length >= 2 && bytes[1] == (byte) 0xFF) { + // UTF-16 Big Endian BOM: FE FF + return new InputStreamReader(new ByteArrayInputStream(bytes, 2, length - 2), UTF_16BE); + } + break; + } + case (byte) 0xFF: + { + if (length >= 2 && bytes[1] == (byte) 0xFE) { + if (length >= 4 && bytes[2] == (byte) 0x00 && bytes[3] == (byte) 0x00) { + // UTF-32 Little Endian BOM: FF FE 00 00 + return new InputStreamReader( + new ByteArrayInputStream(bytes, 4, length - 4), "UTF-32LE"); + } + + // UTF-16 Little Endian BOM: FF FE + return new InputStreamReader(new ByteArrayInputStream(bytes, 2, length - 2), UTF_16LE); + } + break; + } + case (byte) 0x00: + { + if (length >= 4 + && bytes[0] == (byte) 0x00 + && bytes[1] == (byte) 0x00 + && bytes[2] == (byte) 0xFE + && bytes[3] == (byte) 0xFF) { + // UTF-32 Big Endian BOM: 00 00 FE FF + return new InputStreamReader( + new ByteArrayInputStream(bytes, 4, length - 4), "UTF-32BE"); + } + break; + } + } + + // No byte order mark: Assume UTF-8 (where the BOM is optional). + return new InputStreamReader(new ByteArrayInputStream(bytes), UTF_8); + } + + public static class ShrinkerResult { + private final Set<String> resFolderEntriesToKeep; + private final byte[] resourceTableInProtoFormat; + + public ShrinkerResult(Set<String> resFolderEntriesToKeep, byte[] resourceTableInProtoFormat) { + this.resFolderEntriesToKeep = resFolderEntriesToKeep; + this.resourceTableInProtoFormat = resourceTableInProtoFormat; + } + + public byte[] getResourceTableInProtoFormat() { + return resourceTableInProtoFormat; + } + + public Set<String> getResFolderEntriesToKeep() { + return resFolderEntriesToKeep; + } + } + + private static class PathAndBytes { + private final byte[] bytes; + private final Path path; + + private PathAndBytes(byte[] bytes, Path path) { + this.bytes = bytes; + this.path = path; + } + + public Path getPath() { + return path; + } + + public String getPathWithoutRes() { + assert path.toString().startsWith("res/"); + return path.toString().substring(4); + } + + public byte[] getBytes() { + return bytes; + } + } +}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java index 5f9bec6..d8ac6a9 100644 --- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java +++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -32,7 +32,7 @@ r8ResourceShrinkerModel.instantiateFromResourceTable(inputStream); } - private static class R8ResourceShrinkerModel extends ResourceShrinkerModel { + public static class R8ResourceShrinkerModel extends ResourceShrinkerModel { public R8ResourceShrinkerModel( ShrinkerDebugReporter debugReporter, boolean supportMultipackages) { @@ -40,25 +40,29 @@ } // Similar to instantiation in ProtoResourceTableGatherer, but using an inputstream. - public void instantiateFromResourceTable(InputStream inputStream) { + void instantiateFromResourceTable(InputStream inputStream) { try { ResourceTable resourceTable = ResourceTable.parseFrom(inputStream); - ResourceTableUtilKt.entriesSequence(resourceTable) - .iterator() - .forEachRemaining( - entryWrapper -> { - ResourceType resourceType = ResourceType.fromClassName(entryWrapper.getType()); - if (resourceType != ResourceType.STYLEABLE) { - this.addResource( - resourceType, - entryWrapper.getPackageName(), - ResourcesUtil.resourceNameToFieldName(entryWrapper.getEntry().getName()), - entryWrapper.getId()); - } - }); + instantiateFromResourceTable(resourceTable); } catch (IOException ex) { throw new RuntimeException(ex); } } + + void instantiateFromResourceTable(ResourceTable resourceTable) { + ResourceTableUtilKt.entriesSequence(resourceTable) + .iterator() + .forEachRemaining( + entryWrapper -> { + ResourceType resourceType = ResourceType.fromClassName(entryWrapper.getType()); + if (resourceType != ResourceType.STYLEABLE) { + this.addResource( + resourceType, + entryWrapper.getPackageName(), + ResourcesUtil.resourceNameToFieldName(entryWrapper.getEntry().getName()), + entryWrapper.getId()); + } + }); + } } }