xref: /aosp_15_r20/external/angle/build/android/unused_resources/UnusedResources.java (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 // Modifications are owned by the Chromium Authors.
18 // Copyright 2021 The Chromium Authors
19 // Use of this source code is governed by a BSD-style license that can be
20 // found in the LICENSE file.
21 
22 package build.android.unused_resources;
23 
24 import static com.android.ide.common.symbols.SymbolIo.readFromAapt;
25 import static com.android.utils.SdkUtils.endsWithIgnoreCase;
26 
27 import static com.google.common.base.Charsets.UTF_8;
28 
29 import com.android.ide.common.resources.usage.ResourceUsageModel;
30 import com.android.ide.common.resources.usage.ResourceUsageModel.Resource;
31 import com.android.ide.common.symbols.Symbol;
32 import com.android.ide.common.symbols.SymbolTable;
33 import com.android.resources.ResourceFolderType;
34 import com.android.resources.ResourceType;
35 import com.android.tools.r8.CompilationFailedException;
36 import com.android.tools.r8.ProgramResource;
37 import com.android.tools.r8.ProgramResourceProvider;
38 import com.android.tools.r8.ResourceShrinker;
39 import com.android.tools.r8.ResourceShrinker.Command;
40 import com.android.tools.r8.ResourceShrinker.ReferenceChecker;
41 import com.android.tools.r8.origin.PathOrigin;
42 import com.android.utils.XmlUtils;
43 
44 import com.google.common.base.Charsets;
45 import com.google.common.collect.Maps;
46 import com.google.common.io.ByteStreams;
47 import com.google.common.io.Closeables;
48 import com.google.common.io.Files;
49 
50 import org.w3c.dom.Document;
51 import org.w3c.dom.Node;
52 import org.xml.sax.SAXException;
53 
54 import java.io.File;
55 import java.io.FileInputStream;
56 import java.io.IOException;
57 import java.io.PrintWriter;
58 import java.io.StringWriter;
59 import java.nio.file.Path;
60 import java.nio.file.Paths;
61 import java.util.Arrays;
62 import java.util.Collections;
63 import java.util.List;
64 import java.util.Map;
65 import java.util.concurrent.ExecutionException;
66 import java.util.stream.Collectors;
67 import java.util.zip.ZipEntry;
68 import java.util.zip.ZipInputStream;
69 
70 import javax.xml.parsers.ParserConfigurationException;
71 
72 /**
73   Copied with modifications from gradle core source
74   https://cs.android.com/search?q=f:build-system.*ResourceUsageAnalyzer.java
75 
76   Modifications are mostly to:
77     - Remove unused code paths to reduce complexity.
78     - Reduce dependencies unless absolutely required.
79 */
80 
81 public class UnusedResources {
82     private static final String ANDROID_RES = "android_res/";
83     private static final String DOT_DEX = ".dex";
84     private static final String DOT_CLASS = ".class";
85     private static final String DOT_XML = ".xml";
86     private static final String DOT_JAR = ".jar";
87     private static final String FN_RESOURCE_TEXT = "R.txt";
88 
89     /* A source of resource classes to track, can be either a folder or a jar */
90     private final Iterable<File> mRTxtFiles;
91     private final File mProguardMapping;
92     /** These can be class or dex files. */
93     private final Iterable<File> mClasses;
94     private final Iterable<File> mManifests;
95     private final Iterable<File> mResourceDirs;
96 
97     private final File mReportFile;
98     private final StringWriter mDebugOutput;
99     private final PrintWriter mDebugPrinter;
100 
101     /** The computed set of unused resources */
102     private List<Resource> mUnused;
103 
104     /**
105      * Map from resource class owners (VM format class) to corresponding resource entries.
106      * This lets us map back from code references (obfuscated class and possibly obfuscated field
107      * reference) back to the corresponding resource type and name.
108      */
109     private Map<String, Pair<ResourceType, Map<String, String>>> mResourceObfuscation =
110             Maps.newHashMapWithExpectedSize(30);
111 
112     /** Obfuscated name of android/support/v7/widget/SuggestionsAdapter.java */
113     private String mSuggestionsAdapter;
114 
115     /** Obfuscated name of android/support/v7/internal/widget/ResourcesWrapper.java */
116     private String mResourcesWrapper;
117 
118     /* A Pair class because java does not come with batteries included. */
119     private static class Pair<U, V> {
120         private U mFirst;
121         private V mSecond;
122 
Pair(U first, V second)123         Pair(U first, V second) {
124             this.mFirst = first;
125             this.mSecond = second;
126         }
127 
getFirst()128         public U getFirst() {
129             return mFirst;
130         }
131 
getSecond()132         public V getSecond() {
133             return mSecond;
134         }
135     }
136 
UnusedResources(Iterable<File> rTxtFiles, Iterable<File> classes, Iterable<File> manifests, File mapping, Iterable<File> resources, File reportFile)137     public UnusedResources(Iterable<File> rTxtFiles, Iterable<File> classes,
138             Iterable<File> manifests, File mapping, Iterable<File> resources, File reportFile) {
139         mRTxtFiles = rTxtFiles;
140         mProguardMapping = mapping;
141         mClasses = classes;
142         mManifests = manifests;
143         mResourceDirs = resources;
144 
145         mReportFile = reportFile;
146         if (reportFile != null) {
147             mDebugOutput = new StringWriter(8 * 1024);
148             mDebugPrinter = new PrintWriter(mDebugOutput);
149         } else {
150             mDebugOutput = null;
151             mDebugPrinter = null;
152         }
153     }
154 
close()155     public void close() {
156         if (mDebugOutput != null) {
157             String output = mDebugOutput.toString();
158 
159             if (mReportFile != null) {
160                 File dir = mReportFile.getParentFile();
161                 if (dir != null) {
162                     if ((dir.exists() || dir.mkdir()) && dir.canWrite()) {
163                         try {
164                             Files.asCharSink(mReportFile, Charsets.UTF_8).write(output);
165                         } catch (IOException ignore) {
166                         }
167                     }
168                 }
169             }
170         }
171     }
172 
analyze()173     public void analyze() throws IOException, ParserConfigurationException, SAXException {
174         gatherResourceValues(mRTxtFiles);
175         recordMapping(mProguardMapping);
176 
177         for (File jarOrDir : mClasses) {
178             recordClassUsages(jarOrDir);
179         }
180         recordManifestUsages(mManifests);
181         recordResources(mResourceDirs);
182         dumpReferences();
183         mModel.processToolsAttributes();
184         mUnused = mModel.findUnused();
185     }
186 
emitConfig(Path destination)187     public void emitConfig(Path destination) throws IOException {
188         File destinationFile = destination.toFile();
189         if (!destinationFile.exists()) {
190             destinationFile.getParentFile().mkdirs();
191             boolean success = destinationFile.createNewFile();
192             if (!success) {
193                 throw new IOException("Could not create " + destination);
194             }
195         }
196         StringBuilder sb = new StringBuilder();
197         Collections.sort(mUnused);
198         for (Resource resource : mUnused) {
199             if (resource.type.isSynthetic()) {
200                 // Ignore synthetic resources like overlayable or macro that are
201                 // not actually listed in the ResourceTable.
202                 continue;
203             }
204             sb.append(resource.type + "/" + resource.name + "#remove\n");
205         }
206         Files.asCharSink(destinationFile, UTF_8).write(sb.toString());
207     }
208 
dumpReferences()209     private void dumpReferences() {
210         if (mDebugPrinter != null) {
211             mDebugPrinter.print(mModel.dumpReferences());
212         }
213     }
214 
dumpModel()215     private void dumpModel() {
216         if (mDebugPrinter != null) {
217             mDebugPrinter.print(mModel.dumpResourceModel());
218         }
219     }
220 
recordResources(Iterable<File> resources)221     private void recordResources(Iterable<File> resources)
222             throws IOException, SAXException, ParserConfigurationException {
223         for (File resDir : resources) {
224             File[] resourceFolders = resDir.listFiles();
225             assert resourceFolders != null : "Invalid resource directory " + resDir;
226             for (File folder : resourceFolders) {
227                 ResourceFolderType folderType = ResourceFolderType.getFolderType(folder.getName());
228                 if (folderType != null) {
229                     recordResources(folderType, folder);
230                 }
231             }
232         }
233     }
234 
recordResources(ResourceFolderType folderType, File folder)235     private void recordResources(ResourceFolderType folderType, File folder)
236             throws ParserConfigurationException, SAXException, IOException {
237         File[] files = folder.listFiles();
238         if (files != null) {
239             for (File file : files) {
240                 String path = file.getPath();
241                 mModel.file = file;
242                 try {
243                     boolean isXml = endsWithIgnoreCase(path, DOT_XML);
244                     if (isXml) {
245                         String xml = Files.toString(file, UTF_8);
246                         Document document = XmlUtils.parseDocument(xml, true);
247                         mModel.visitXmlDocument(file, folderType, document);
248                     } else {
249                         mModel.visitBinaryResource(folderType, file);
250                     }
251                 } finally {
252                     mModel.file = null;
253                 }
254             }
255         }
256     }
257 
recordMapping(File mapping)258     void recordMapping(File mapping) throws IOException {
259         if (mapping == null || !mapping.exists()) {
260             return;
261         }
262         final String arrowString = " -> ";
263         final String resourceString = ".R$";
264         Map<String, String> nameMap = null;
265         for (String line : Files.readLines(mapping, UTF_8)) {
266             // Ignore R8's mapping comments.
267             if (line.startsWith("#")) {
268                 continue;
269             }
270             if (line.startsWith(" ") || line.startsWith("\t")) {
271                 if (nameMap != null) {
272                     // We're processing the members of a resource class: record names into the map
273                     int n = line.length();
274                     int i = 0;
275                     for (; i < n; i++) {
276                         if (!Character.isWhitespace(line.charAt(i))) {
277                             break;
278                         }
279                     }
280                     if (i < n && line.startsWith("int", i)) { // int or int[]
281                         int start = line.indexOf(' ', i + 3) + 1;
282                         int arrow = line.indexOf(arrowString);
283                         if (start > 0 && arrow != -1) {
284                             int end = line.indexOf(' ', start + 1);
285                             if (end != -1) {
286                                 String oldName = line.substring(start, end);
287                                 String newName =
288                                         line.substring(arrow + arrowString.length()).trim();
289                                 if (!newName.equals(oldName)) {
290                                     nameMap.put(newName, oldName);
291                                 }
292                             }
293                         }
294                     }
295                 }
296                 continue;
297             } else {
298                 nameMap = null;
299             }
300             int index = line.indexOf(resourceString);
301             if (index == -1) {
302                 // Record obfuscated names of a few known appcompat usages of
303                 // Resources#getIdentifier that are unlikely to be used for general
304                 // resource name reflection
305                 if (line.startsWith("android.support.v7.widget.SuggestionsAdapter ")) {
306                     mSuggestionsAdapter =
307                             line.substring(line.indexOf(arrowString) + arrowString.length(),
308                                         line.indexOf(':') != -1 ? line.indexOf(':') : line.length())
309                                     .trim()
310                                     .replace('.', '/')
311                             + DOT_CLASS;
312                 } else if (line.startsWith("android.support.v7.internal.widget.ResourcesWrapper ")
313                         || line.startsWith("android.support.v7.widget.ResourcesWrapper ")
314                         || (mResourcesWrapper == null // Recently wrapper moved
315                                 && line.startsWith(
316                                         "android.support.v7.widget.TintContextWrapper$TintResources"
317                                                 + " "))) {
318                     mResourcesWrapper =
319                             line.substring(line.indexOf(arrowString) + arrowString.length(),
320                                         line.indexOf(':') != -1 ? line.indexOf(':') : line.length())
321                                     .trim()
322                                     .replace('.', '/')
323                             + DOT_CLASS;
324                 }
325                 continue;
326             }
327             int arrow = line.indexOf(arrowString, index + 3);
328             if (arrow == -1) {
329                 continue;
330             }
331             String typeName = line.substring(index + resourceString.length(), arrow);
332             ResourceType type = ResourceType.fromClassName(typeName);
333             if (type == null) {
334                 continue;
335             }
336             int end = line.indexOf(':', arrow + arrowString.length());
337             if (end == -1) {
338                 end = line.length();
339             }
340             String target = line.substring(arrow + arrowString.length(), end).trim();
341             String ownerName = target.replace('.', '/');
342 
343             nameMap = Maps.newHashMap();
344             Pair<ResourceType, Map<String, String>> pair = new Pair(type, nameMap);
345             mResourceObfuscation.put(ownerName, pair);
346             // For fast lookup in isResourceClass
347             mResourceObfuscation.put(ownerName + DOT_CLASS, pair);
348         }
349     }
350 
recordManifestUsages(File manifest)351     private void recordManifestUsages(File manifest)
352             throws IOException, ParserConfigurationException, SAXException {
353         String xml = Files.toString(manifest, UTF_8);
354         Document document = XmlUtils.parseDocument(xml, true);
355         mModel.visitXmlDocument(manifest, null, document);
356     }
357 
recordManifestUsages(Iterable<File> manifests)358     private void recordManifestUsages(Iterable<File> manifests)
359             throws IOException, ParserConfigurationException, SAXException {
360         for (File manifest : manifests) {
361             recordManifestUsages(manifest);
362         }
363     }
364 
recordClassUsages(File file)365     private void recordClassUsages(File file) throws IOException {
366         assert file.isFile();
367         if (file.getPath().endsWith(DOT_DEX)) {
368             byte[] bytes = Files.toByteArray(file);
369             recordClassUsages(file, file.getName(), bytes);
370         } else if (file.getPath().endsWith(DOT_JAR)) {
371             ZipInputStream zis = null;
372             try {
373                 FileInputStream fis = new FileInputStream(file);
374                 try {
375                     zis = new ZipInputStream(fis);
376                     ZipEntry entry = zis.getNextEntry();
377                     while (entry != null) {
378                         String name = entry.getName();
379                         if (name.endsWith(DOT_DEX)) {
380                             byte[] bytes = ByteStreams.toByteArray(zis);
381                             if (bytes != null) {
382                                 recordClassUsages(file, name, bytes);
383                             }
384                         }
385 
386                         entry = zis.getNextEntry();
387                     }
388                 } finally {
389                     Closeables.close(fis, true);
390                 }
391             } finally {
392                 Closeables.close(zis, true);
393             }
394         }
395     }
396 
stringifyResource(Resource resource)397     private String stringifyResource(Resource resource) {
398         return String.format("%s:%s:0x%08x", resource.type, resource.name, resource.value);
399     }
400 
recordClassUsages(File file, String name, byte[] bytes)401     private void recordClassUsages(File file, String name, byte[] bytes) {
402         assert name.endsWith(DOT_DEX);
403         ReferenceChecker callback = new ReferenceChecker() {
404             @Override
405             public boolean shouldProcess(String internalName) {
406                 // We do not need to ignore R subclasses since R8 now removes
407                 // unused resource id fields in R subclasses thus their
408                 // remaining presence means real usage.
409                 return true;
410             }
411 
412             @Override
413             public void referencedInt(int value) {
414                 UnusedResources.this.referencedInt("dex", value, file, name);
415             }
416 
417             @Override
418             public void referencedString(String value) {
419                 // do nothing.
420             }
421 
422             @Override
423             public void referencedStaticField(String internalName, String fieldName) {
424                 Resource resource = getResourceFromCode(internalName, fieldName);
425                 if (resource != null) {
426                     ResourceUsageModel.markReachable(resource);
427                     if (mDebugPrinter != null) {
428                         mDebugPrinter.println("Marking " + stringifyResource(resource)
429                                 + " reachable: referenced from dex"
430                                 + " in " + file + ":" + name + " (static field access "
431                                 + internalName + "." + fieldName + ")");
432                     }
433                 }
434             }
435 
436             @Override
437             public void referencedMethod(
438                     String internalName, String methodName, String methodDescriptor) {
439                 // Do nothing.
440             }
441         };
442         ProgramResource resource = ProgramResource.fromBytes(
443                 new PathOrigin(file.toPath()), ProgramResource.Kind.DEX, bytes, null);
444         ProgramResourceProvider provider = () -> Arrays.asList(resource);
445         try {
446             Command command =
447                     (new ResourceShrinker.Builder()).addProgramResourceProvider(provider).build();
448             ResourceShrinker.run(command, callback);
449         } catch (CompilationFailedException e) {
450             e.printStackTrace();
451         } catch (IOException e) {
452             e.printStackTrace();
453         } catch (ExecutionException e) {
454             e.printStackTrace();
455         }
456     }
457 
458     /** Returns whether the given class file name points to an aapt-generated compiled R class. */
isResourceClass(String name)459     boolean isResourceClass(String name) {
460         if (mResourceObfuscation.containsKey(name)) {
461             return true;
462         }
463         int index = name.lastIndexOf('/');
464         if (index != -1 && name.startsWith("R$", index + 1) && name.endsWith(DOT_CLASS)) {
465             String typeName = name.substring(index + 3, name.length() - DOT_CLASS.length());
466             return ResourceType.fromClassName(typeName) != null;
467         }
468         return false;
469     }
470 
getResourceFromCode(String owner, String name)471     Resource getResourceFromCode(String owner, String name) {
472         Pair<ResourceType, Map<String, String>> pair = mResourceObfuscation.get(owner);
473         if (pair != null) {
474             ResourceType type = pair.getFirst();
475             Map<String, String> nameMap = pair.getSecond();
476             String renamedField = nameMap.get(name);
477             if (renamedField != null) {
478                 name = renamedField;
479             }
480             return mModel.getResource(type, name);
481         }
482         if (isValidResourceType(owner)) {
483             ResourceType type =
484                     ResourceType.fromClassName(owner.substring(owner.lastIndexOf('$') + 1));
485             if (type != null) {
486                 return mModel.getResource(type, name);
487             }
488         }
489         return null;
490     }
491 
isValidResourceType(String candidateString)492     private Boolean isValidResourceType(String candidateString) {
493         return candidateString.contains("/")
494                 && candidateString.substring(candidateString.lastIndexOf('/') + 1).contains("$");
495     }
496 
gatherResourceValues(Iterable<File> rTxts)497     private void gatherResourceValues(Iterable<File> rTxts) throws IOException {
498         for (File rTxt : rTxts) {
499             assert rTxt.isFile();
500             assert rTxt.getName().endsWith(FN_RESOURCE_TEXT);
501             addResourcesFromRTxtFile(rTxt);
502         }
503     }
504 
addResourcesFromRTxtFile(File file)505     private void addResourcesFromRTxtFile(File file) {
506         try {
507             SymbolTable st = readFromAapt(file, null);
508             for (Symbol symbol : st.getSymbols().values()) {
509                 String symbolValue = symbol.getValue();
510                 if (symbol.getResourceType() == ResourceType.STYLEABLE) {
511                     if (symbolValue.trim().startsWith("{")) {
512                         // Only add the styleable parent, styleable children are not yet supported.
513                         mModel.addResource(symbol.getResourceType(), symbol.getName(), null);
514                     }
515                 } else {
516                     if (mDebugPrinter != null) {
517                         mDebugPrinter.println("Extracted R.txt resource: "
518                                 + symbol.getResourceType() + ":" + symbol.getName() + ":"
519                                 + String.format(
520                                         "0x%08x", Integer.parseInt(symbolValue.substring(2), 16)));
521                     }
522                     mModel.addResource(symbol.getResourceType(), symbol.getName(), symbolValue);
523                 }
524             }
525         } catch (Exception e) {
526             e.printStackTrace();
527         }
528     }
529 
getModel()530     ResourceUsageModel getModel() {
531         return mModel;
532     }
533 
referencedInt(String context, int value, File file, String currentClass)534     private void referencedInt(String context, int value, File file, String currentClass) {
535         Resource resource = mModel.getResource(value);
536         if (ResourceUsageModel.markReachable(resource) && mDebugPrinter != null) {
537             mDebugPrinter.println("Marking " + stringifyResource(resource)
538                     + " reachable: referenced from " + context + " in " + file + ":"
539                     + currentClass);
540         }
541     }
542 
543     private final ResourceShrinkerUsageModel mModel = new ResourceShrinkerUsageModel();
544 
545     private class ResourceShrinkerUsageModel extends ResourceUsageModel {
546         public File file;
547 
548         /**
549          * Whether we should ignore tools attribute resource references.
550          * <p>
551          * For example, for resource shrinking we want to ignore tools attributes,
552          * whereas for resource refactoring on the source code we do not.
553          *
554          * @return whether tools attributes should be ignored
555          */
556         @Override
ignoreToolsAttributes()557         protected boolean ignoreToolsAttributes() {
558             return true;
559         }
560 
561         @Override
onRootResourcesFound(List<Resource> roots)562         protected void onRootResourcesFound(List<Resource> roots) {
563             if (mDebugPrinter != null) {
564                 mDebugPrinter.println("\nThe root reachable resources are:");
565                 for (Resource root : roots) {
566                     mDebugPrinter.println("   " + stringifyResource(root) + ",");
567                 }
568             }
569         }
570 
571         @Override
declareResource(ResourceType type, String name, Node node)572         protected Resource declareResource(ResourceType type, String name, Node node) {
573             Resource resource = super.declareResource(type, name, node);
574             resource.addLocation(file);
575             return resource;
576         }
577 
578         @Override
referencedString(String string)579         protected void referencedString(String string) {
580             // Do nothing
581         }
582     }
583 
parsePathsFromFile(String path)584     private static List<File> parsePathsFromFile(String path) throws IOException {
585         return java.nio.file.Files.readAllLines(new File(path).toPath()).stream()
586                 .map(File::new)
587                 .collect(Collectors.toList());
588     }
589 
main(String[] args)590     public static void main(String[] args) throws Exception {
591         List<File> rTxtFiles = null; // R.txt files
592         List<File> classes = null; // Dex/jar w dex
593         List<File> manifests = null; // manifests
594         File mapping = null; // mapping
595         List<File> resources = null; // resources dirs
596         File log = null; // output log for debugging
597         Path configPath = null; // output config
598         for (int i = 0; i < args.length; i += 2) {
599             switch (args[i]) {
600                 case "--rtxts":
601                     rTxtFiles = Arrays.stream(args[i + 1].split(":"))
602                                         .map(s -> new File(s))
603                                         .collect(Collectors.toList());
604                     break;
605                 case "--dexes":
606                     classes = parsePathsFromFile(args[i + 1]);
607                     break;
608                 case "--manifests":
609                     manifests = parsePathsFromFile(args[i + 1]);
610                     break;
611                 case "--mapping":
612                     mapping = new File(args[i + 1]);
613                     break;
614                 case "--resourceDirs":
615                     resources = parsePathsFromFile(args[i + 1]);
616                     break;
617                 case "--log":
618                     log = new File(args[i + 1]);
619                     break;
620                 case "--outputConfig":
621                     configPath = Paths.get(args[i + 1]);
622                     break;
623                 default:
624                     throw new IllegalArgumentException(args[i] + " is not a valid arg.");
625             }
626         }
627         UnusedResources unusedResources =
628                 new UnusedResources(rTxtFiles, classes, manifests, mapping, resources, log);
629         unusedResources.analyze();
630         unusedResources.close();
631         unusedResources.emitConfig(configPath);
632     }
633 }
634