blob: a64a3fd28f68a729f0c8fe852531069a337bb192 [file] [log] [blame]
Rico Wind441f03d2023-10-26 12:40:07 +02001// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
2// for details. All rights reserved. Use of this source code is governed by a
3// BSD-style license that can be found in the LICENSE file.
4
5package com.android.build.shrinker.r8integration;
6
7import static java.nio.charset.StandardCharsets.UTF_16BE;
8import static java.nio.charset.StandardCharsets.UTF_16LE;
9import static java.nio.charset.StandardCharsets.UTF_8;
10
11import com.android.aapt.Resources.ResourceTable;
12import com.android.aapt.Resources.XmlNode;
Rico Wind441f03d2023-10-26 12:40:07 +020013import com.android.build.shrinker.ResourceShrinkerImplKt;
14import com.android.build.shrinker.ResourceTableUtilKt;
Rico Windc5b44012024-08-16 13:30:59 +020015import com.android.build.shrinker.ShrinkerDebugReporter;
Rico Wind441f03d2023-10-26 12:40:07 +020016import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder;
Rico Wind7eaee652024-01-05 09:47:54 +010017import com.android.build.shrinker.obfuscation.ProguardMappingsRecorder;
Rico Wind441f03d2023-10-26 12:40:07 +020018import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel;
19import com.android.build.shrinker.usages.DexFileAnalysisCallback;
20import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorderKt;
21import com.android.build.shrinker.usages.R8ResourceShrinker;
22import com.android.build.shrinker.usages.ToolsAttributeUsageRecorderKt;
Rico Windc25e07e2023-10-30 08:41:12 +010023import com.android.ide.common.resources.ResourcesUtil;
Rico Wind441f03d2023-10-26 12:40:07 +020024import com.android.ide.common.resources.usage.ResourceStore;
Rico Windee465122024-01-15 09:41:09 +010025import com.android.ide.common.resources.usage.ResourceUsageModel.Resource;
Rico Wind26aaf902024-01-29 12:59:53 +010026import com.android.resources.ResourceType;
Rico Windcc52b7f2023-11-09 11:29:54 +010027import com.android.tools.r8.FeatureSplit;
Rico Wind441f03d2023-10-26 12:40:07 +020028import com.google.common.collect.ImmutableMap;
29import com.google.common.collect.ImmutableSet;
30import com.google.common.collect.Iterables;
Rico Windcc52b7f2023-11-09 11:29:54 +010031import com.google.protobuf.InvalidProtocolBufferException;
Rico Wind441f03d2023-10-26 12:40:07 +020032import java.io.ByteArrayInputStream;
33import java.io.IOException;
34import java.io.InputStreamReader;
35import java.io.Reader;
36import java.io.StringReader;
37import java.nio.file.Path;
38import java.nio.file.Paths;
39import java.util.ArrayList;
Rico Wind5b0579f2024-08-13 08:46:59 +020040import java.util.Arrays;
41import java.util.Collection;
Rico Wind441f03d2023-10-26 12:40:07 +020042import java.util.HashMap;
43import java.util.List;
44import java.util.Map;
45import java.util.Map.Entry;
46import java.util.Set;
47import java.util.stream.Collectors;
48import javax.xml.parsers.ParserConfigurationException;
Rico Wind441f03d2023-10-26 12:40:07 +020049import org.xml.sax.SAXException;
50
51public class LegacyResourceShrinker {
Rico Windcc52b7f2023-11-09 11:29:54 +010052 private final Map<String, byte[]> dexInputs;
Rico Wind5b0579f2024-08-13 08:46:59 +020053 private final Collection<PathAndBytes> resFolderInputs;
54 private final Collection<PathAndBytes> xmlInputs;
Rico Windfd460d22024-08-20 11:06:18 +020055 private final List<byte[]> keepRuleInput;
Rico Wind7eaee652024-01-05 09:47:54 +010056 private List<String> proguardMapStrings;
Rico Windc5b44012024-08-16 13:30:59 +020057 private final ShrinkerDebugReporter debugReporter;
Rico Windcc52b7f2023-11-09 11:29:54 +010058 private final List<PathAndBytes> manifest;
59 private final Map<PathAndBytes, FeatureSplit> resourceTables;
Rico Wind441f03d2023-10-26 12:40:07 +020060
61 public static class Builder {
62
Rico Windcc52b7f2023-11-09 11:29:54 +010063 private final Map<String, byte[]> dexInputs = new HashMap<>();
Rico Wind5d22f0d2024-09-19 13:23:04 +020064 private final Map<String, PathAndBytes> resFolderInputs = new HashMap<>();
65 private final Map<String, PathAndBytes> xmlInputs = new HashMap<>();
Rico Windfd460d22024-08-20 11:06:18 +020066 private final List<byte[]> keepRuleInput = new ArrayList<>();
Rico Wind441f03d2023-10-26 12:40:07 +020067
Rico Windcc52b7f2023-11-09 11:29:54 +010068 private final List<PathAndBytes> manifests = new ArrayList<>();
69 private final Map<PathAndBytes, FeatureSplit> resourceTables = new HashMap<>();
Rico Wind7eaee652024-01-05 09:47:54 +010070 private List<String> proguardMapStrings;
Rico Windc5b44012024-08-16 13:30:59 +020071 private ShrinkerDebugReporter debugReporter;
Rico Wind441f03d2023-10-26 12:40:07 +020072
73 private Builder() {}
74
Rico Wind5d22f0d2024-09-19 13:23:04 +020075 public Builder addManifest(String path, byte[] bytes) {
Rico Windcc52b7f2023-11-09 11:29:54 +010076 manifests.add(new PathAndBytes(bytes, path));
Rico Wind441f03d2023-10-26 12:40:07 +020077 return this;
78 }
79
Rico Wind5d22f0d2024-09-19 13:23:04 +020080 public Builder addResourceTable(String path, byte[] bytes, FeatureSplit featureSplit) {
Rico Windcc52b7f2023-11-09 11:29:54 +010081 resourceTables.put(new PathAndBytes(bytes, path), featureSplit);
82 try {
83 ResourceTable resourceTable = ResourceTable.parseFrom(bytes);
84 System.currentTimeMillis();
85 } catch (InvalidProtocolBufferException e) {
86 throw new RuntimeException(e);
87 }
Rico Wind441f03d2023-10-26 12:40:07 +020088 return this;
89 }
90
Rico Windcc52b7f2023-11-09 11:29:54 +010091 public Builder addDexInput(String classesLocation, byte[] bytes) {
92 dexInputs.put(classesLocation, bytes);
Rico Wind441f03d2023-10-26 12:40:07 +020093 return this;
94 }
95
Rico Windfd460d22024-08-20 11:06:18 +020096 public Builder addKeepRuleInput(byte[] bytes) {
97 keepRuleInput.add(bytes);
98 return this;
99 }
100
Rico Wind5d22f0d2024-09-19 13:23:04 +0200101 public Builder addResFolderInput(String path, byte[] bytes) {
Rico Wind5b0579f2024-08-13 08:46:59 +0200102 PathAndBytes existing = resFolderInputs.get(path);
103 if (existing != null) {
Rico Wind32420ca2024-12-19 15:04:52 +0100104 existing.setDuplicated(true);
Rico Wind5b0579f2024-08-13 08:46:59 +0200105 } else {
106 resFolderInputs.put(path, new PathAndBytes(bytes, path));
107 }
Rico Wind441f03d2023-10-26 12:40:07 +0200108 return this;
109 }
110
Rico Wind5d22f0d2024-09-19 13:23:04 +0200111 public Builder addXmlInput(String path, byte[] bytes) {
Rico Wind5b0579f2024-08-13 08:46:59 +0200112 PathAndBytes existing = xmlInputs.get(path);
113 if (existing != null) {
Rico Wind32420ca2024-12-19 15:04:52 +0100114 existing.setDuplicated(true);
Rico Wind5b0579f2024-08-13 08:46:59 +0200115 } else {
116 xmlInputs.put(path, new PathAndBytes(bytes, path));
117 }
Rico Wind441f03d2023-10-26 12:40:07 +0200118 return this;
119 }
120
121 public LegacyResourceShrinker build() {
Rico Windcc52b7f2023-11-09 11:29:54 +0100122 assert manifests != null && resourceTables != null;
Rico Wind441f03d2023-10-26 12:40:07 +0200123 return new LegacyResourceShrinker(
Rico Wind5b0579f2024-08-13 08:46:59 +0200124 dexInputs,
125 resFolderInputs.values(),
126 manifests,
127 resourceTables,
128 xmlInputs.values(),
Rico Windfd460d22024-08-20 11:06:18 +0200129 keepRuleInput,
Rico Windc5b44012024-08-16 13:30:59 +0200130 proguardMapStrings,
131 debugReporter);
Rico Wind7eaee652024-01-05 09:47:54 +0100132 }
133
134 public void setProguardMapStrings(List<String> proguardMapStrings) {
135 this.proguardMapStrings = proguardMapStrings;
Rico Wind441f03d2023-10-26 12:40:07 +0200136 }
Rico Windc5b44012024-08-16 13:30:59 +0200137
138 public Builder setShrinkerDebugReporter(ShrinkerDebugReporter debugReporter) {
139 this.debugReporter = debugReporter;
140 return this;
141 }
Rico Wind441f03d2023-10-26 12:40:07 +0200142 }
143
144 private LegacyResourceShrinker(
Rico Windcc52b7f2023-11-09 11:29:54 +0100145 Map<String, byte[]> dexInputs,
Rico Wind5b0579f2024-08-13 08:46:59 +0200146 Collection<PathAndBytes> resFolderInputs,
Rico Windcc52b7f2023-11-09 11:29:54 +0100147 List<PathAndBytes> manifests,
148 Map<PathAndBytes, FeatureSplit> resourceTables,
Rico Wind5b0579f2024-08-13 08:46:59 +0200149 Collection<PathAndBytes> xmlInputs,
Rico Windfd460d22024-08-20 11:06:18 +0200150 List<byte[]> additionalRawXmlInputs,
Rico Windc5b44012024-08-16 13:30:59 +0200151 List<String> proguardMapStrings,
152 ShrinkerDebugReporter debugReporter) {
Rico Wind441f03d2023-10-26 12:40:07 +0200153 this.dexInputs = dexInputs;
154 this.resFolderInputs = resFolderInputs;
Rico Windcc52b7f2023-11-09 11:29:54 +0100155 this.manifest = manifests;
156 this.resourceTables = resourceTables;
Rico Wind441f03d2023-10-26 12:40:07 +0200157 this.xmlInputs = xmlInputs;
Rico Windfd460d22024-08-20 11:06:18 +0200158 this.keepRuleInput = additionalRawXmlInputs;
Rico Wind7eaee652024-01-05 09:47:54 +0100159 this.proguardMapStrings = proguardMapStrings;
Rico Windc5b44012024-08-16 13:30:59 +0200160 this.debugReporter = debugReporter;
Rico Wind441f03d2023-10-26 12:40:07 +0200161 }
162
163 public static Builder builder() {
164 return new Builder();
165 }
166
167 public ShrinkerResult run() throws IOException, ParserConfigurationException, SAXException {
Rico Windc5b44012024-08-16 13:30:59 +0200168 R8ResourceShrinkerModel model = new R8ResourceShrinkerModel(debugReporter, true);
Rico Windcc52b7f2023-11-09 11:29:54 +0100169 for (PathAndBytes pathAndBytes : resourceTables.keySet()) {
170 ResourceTable loadedResourceTable = ResourceTable.parseFrom(pathAndBytes.bytes);
Rico Windba8a9a42024-01-15 11:22:01 +0100171 model.instantiateFromResourceTable(loadedResourceTable, false);
Rico Windcc52b7f2023-11-09 11:29:54 +0100172 }
Rico Wind7eaee652024-01-05 09:47:54 +0100173 if (proguardMapStrings != null) {
174 new ProguardMappingsRecorder(proguardMapStrings).recordObfuscationMappings(model);
175 proguardMapStrings = null;
176 }
Rico Windcc52b7f2023-11-09 11:29:54 +0100177 for (Entry<String, byte[]> entry : dexInputs.entrySet()) {
Rico Wind441f03d2023-10-26 12:40:07 +0200178 // The analysis needs an origin for the dex files, synthesize an easy recognizable one.
Rico Windc5b44012024-08-16 13:30:59 +0200179 Path inMemoryR8 = Paths.get("in_memory_r8_" + entry.getKey());
Rico Wind441f03d2023-10-26 12:40:07 +0200180 R8ResourceShrinker.runResourceShrinkerAnalysis(
181 entry.getValue(), inMemoryR8, new DexFileAnalysisCallback(inMemoryR8, model));
182 }
Rico Windcc52b7f2023-11-09 11:29:54 +0100183 for (PathAndBytes pathAndBytes : manifest) {
184 ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode(
185 XmlNode.parseFrom(pathAndBytes.bytes), model);
186 }
Rico Windfd460d22024-08-20 11:06:18 +0200187 for (byte[] keepBytes : keepRuleInput) {
188 ToolsAttributeUsageRecorderKt.processRawXml(getUtfReader(keepBytes), model);
Rico Wind441f03d2023-10-26 12:40:07 +0200189 }
Rico Wind441f03d2023-10-26 12:40:07 +0200190
Rico Windcc52b7f2023-11-09 11:29:54 +0100191 ImmutableMap<String, PathAndBytes> resFolderMappings =
192 new ImmutableMap.Builder<String, PathAndBytes>()
193 .putAll(
194 xmlInputs.stream()
195 .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
196 .putAll(
197 resFolderInputs.stream()
198 .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
199 .build();
200 for (PathAndBytes pathAndBytes : resourceTables.keySet()) {
201 ResourceTable resourceTable = ResourceTable.parseFrom(pathAndBytes.bytes);
202 new ProtoResourcesGraphBuilder(
Rico Wind717e9732024-02-09 10:36:00 +0100203 pathInRes -> resFolderMappings.get(pathInRes).getBytes(), unused -> resourceTable)
Rico Windcc52b7f2023-11-09 11:29:54 +0100204 .buildGraph(model);
205 }
Rico Wind441f03d2023-10-26 12:40:07 +0200206 ResourceStore resourceStore = model.getResourceStore();
207 resourceStore.processToolsAttributes();
208 model.keepPossiblyReferencedResources();
Rico Windc5b44012024-08-16 13:30:59 +0200209 debugReporter.debug(model.getResourceStore()::dumpResourceModel);
Rico Windc25e07e2023-10-30 08:41:12 +0100210 // Transitively mark the reachable resources in the model.
211 // Finds unused resources in provided resources collection.
212 // Marks all used resources as 'reachable' in original collection.
Rico Windee465122024-01-15 09:41:09 +0100213 List<Resource> unusedResources =
Rico Windc5b44012024-08-16 13:30:59 +0200214 ResourcesUtil.findUnusedResources(
215 model.getResourceStore().getResources(),
216 roots -> {
217 debugReporter.debug(() -> "The root reachable resources are:");
218 roots.forEach(root -> debugReporter.debug(() -> " " + root));
219 });
Rico Wind32420ca2024-12-19 15:04:52 +0100220 ImmutableSet.Builder<String> resEntriesToKeepBuilder = new ImmutableSet.Builder<>();
Rico Wind441f03d2023-10-26 12:40:07 +0200221 for (PathAndBytes xmlInput : Iterables.concat(xmlInputs, resFolderInputs)) {
222 if (ResourceShrinkerImplKt.isJarPathReachable(resourceStore, xmlInput.path.toString())) {
Rico Wind32420ca2024-12-19 15:04:52 +0100223 resEntriesToKeepBuilder.add(xmlInput.path.toString());
224 if (xmlInput.duplicated) {
225 // Ensure that we don't remove references to duplicated res folder entries.
226 List<Resource> duplicatedResources =
227 ResourceShrinkerImplKt.getResourcesFor(resourceStore, xmlInput.path.toString());
228 unusedResources.removeAll(duplicatedResources);
229 }
Rico Wind441f03d2023-10-26 12:40:07 +0200230 }
231 }
Rico Wind32420ca2024-12-19 15:04:52 +0100232 debugReporter.debug(() -> "Unused resources are: ");
233 unusedResources.forEach(unused -> debugReporter.debug(() -> " " + unused));
Rico Wind717e9732024-02-09 10:36:00 +0100234 List<Integer> resourceIdsToRemove = getResourceIdsToRemove(unusedResources);
Rico Windcc52b7f2023-11-09 11:29:54 +0100235 Map<FeatureSplit, ResourceTable> shrunkenTables = new HashMap<>();
236 for (Entry<PathAndBytes, FeatureSplit> entry : resourceTables.entrySet()) {
237 ResourceTable shrunkenResourceTable =
238 ResourceTableUtilKt.nullOutEntriesWithIds(
239 ResourceTable.parseFrom(entry.getKey().bytes), resourceIdsToRemove);
240 shrunkenTables.put(entry.getValue(), shrunkenResourceTable);
241 }
Rico Wind32420ca2024-12-19 15:04:52 +0100242 return new ShrinkerResult(resEntriesToKeepBuilder.build(), shrunkenTables);
Rico Wind441f03d2023-10-26 12:40:07 +0200243 }
244
Rico Wind717e9732024-02-09 10:36:00 +0100245 private static List<Integer> getResourceIdsToRemove(List<Resource> unusedResources) {
Rico Wind26aaf902024-01-29 12:59:53 +0100246 return unusedResources.stream()
247 .filter(s -> s.type != ResourceType.ID)
248 .map(resource -> resource.value)
249 .collect(Collectors.toList());
Rico Windba8a9a42024-01-15 11:22:01 +0100250 }
251
Rico Wind441f03d2023-10-26 12:40:07 +0200252 // Lifted from com/android/utils/XmlUtils.java which we can't easily update internal dependency
253 // for.
254 /**
255 * Returns a character reader for the given bytes, which must be a UTF encoded file.
256 *
257 * <p>The reader does not need to be closed by the caller (because the file is read in full in one
258 * shot and the resulting array is then wrapped in a byte array input stream, which does not need
259 * to be closed.)
260 */
261 public static Reader getUtfReader(byte[] bytes) throws IOException {
262 int length = bytes.length;
263 if (length == 0) {
264 return new StringReader("");
265 }
266
267 switch (bytes[0]) {
268 case (byte) 0xEF:
269 {
270 if (length >= 3 && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) {
271 // UTF-8 BOM: EF BB BF: Skip it
272 return new InputStreamReader(new ByteArrayInputStream(bytes, 3, length - 3), UTF_8);
273 }
274 break;
275 }
276 case (byte) 0xFE:
277 {
278 if (length >= 2 && bytes[1] == (byte) 0xFF) {
279 // UTF-16 Big Endian BOM: FE FF
280 return new InputStreamReader(new ByteArrayInputStream(bytes, 2, length - 2), UTF_16BE);
281 }
282 break;
283 }
284 case (byte) 0xFF:
285 {
286 if (length >= 2 && bytes[1] == (byte) 0xFE) {
287 if (length >= 4 && bytes[2] == (byte) 0x00 && bytes[3] == (byte) 0x00) {
288 // UTF-32 Little Endian BOM: FF FE 00 00
289 return new InputStreamReader(
290 new ByteArrayInputStream(bytes, 4, length - 4), "UTF-32LE");
291 }
292
293 // UTF-16 Little Endian BOM: FF FE
294 return new InputStreamReader(new ByteArrayInputStream(bytes, 2, length - 2), UTF_16LE);
295 }
296 break;
297 }
298 case (byte) 0x00:
299 {
300 if (length >= 4
301 && bytes[0] == (byte) 0x00
302 && bytes[1] == (byte) 0x00
303 && bytes[2] == (byte) 0xFE
304 && bytes[3] == (byte) 0xFF) {
305 // UTF-32 Big Endian BOM: 00 00 FE FF
306 return new InputStreamReader(
307 new ByteArrayInputStream(bytes, 4, length - 4), "UTF-32BE");
308 }
309 break;
310 }
311 }
312
313 // No byte order mark: Assume UTF-8 (where the BOM is optional).
314 return new InputStreamReader(new ByteArrayInputStream(bytes), UTF_8);
315 }
316
317 public static class ShrinkerResult {
318 private final Set<String> resFolderEntriesToKeep;
Rico Windcc52b7f2023-11-09 11:29:54 +0100319 private final Map<FeatureSplit, ResourceTable> resourceTableInProtoFormat;
Rico Wind441f03d2023-10-26 12:40:07 +0200320
Rico Windcc52b7f2023-11-09 11:29:54 +0100321 public ShrinkerResult(
322 Set<String> resFolderEntriesToKeep,
323 Map<FeatureSplit, ResourceTable> resourceTableInProtoFormat) {
Rico Wind441f03d2023-10-26 12:40:07 +0200324 this.resFolderEntriesToKeep = resFolderEntriesToKeep;
325 this.resourceTableInProtoFormat = resourceTableInProtoFormat;
326 }
327
Rico Windcc52b7f2023-11-09 11:29:54 +0100328 public byte[] getResourceTableInProtoFormat(FeatureSplit featureSplit) {
329 return resourceTableInProtoFormat.get(featureSplit).toByteArray();
Rico Wind441f03d2023-10-26 12:40:07 +0200330 }
331
332 public Set<String> getResFolderEntriesToKeep() {
333 return resFolderEntriesToKeep;
334 }
335 }
336
337 private static class PathAndBytes {
338 private final byte[] bytes;
Rico Wind5d22f0d2024-09-19 13:23:04 +0200339 private final String path;
Rico Wind32420ca2024-12-19 15:04:52 +0100340 private boolean duplicated;
Rico Wind441f03d2023-10-26 12:40:07 +0200341
Rico Wind5d22f0d2024-09-19 13:23:04 +0200342 private PathAndBytes(byte[] bytes, String path) {
Rico Wind441f03d2023-10-26 12:40:07 +0200343 this.bytes = bytes;
344 this.path = path;
345 }
346
Rico Wind32420ca2024-12-19 15:04:52 +0100347 public void setDuplicated(boolean duplicated) {
348 this.duplicated = duplicated;
349 }
350
Rico Wind441f03d2023-10-26 12:40:07 +0200351 public String getPathWithoutRes() {
Rico Wind5d22f0d2024-09-19 13:23:04 +0200352 assert path.startsWith("res/");
353 return path.substring(4);
Rico Wind441f03d2023-10-26 12:40:07 +0200354 }
355
356 public byte[] getBytes() {
357 return bytes;
358 }
359 }
360}