From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../build/android/unused_resources/BUILD.gn | 19 + .../android/unused_resources/UnusedResources.java | 594 +++++++++++++++++++++ 2 files changed, 613 insertions(+) create mode 100644 third_party/libwebrtc/build/android/unused_resources/BUILD.gn create mode 100644 third_party/libwebrtc/build/android/unused_resources/UnusedResources.java (limited to 'third_party/libwebrtc/build/android/unused_resources') diff --git a/third_party/libwebrtc/build/android/unused_resources/BUILD.gn b/third_party/libwebrtc/build/android/unused_resources/BUILD.gn new file mode 100644 index 0000000000..15961048bd --- /dev/null +++ b/third_party/libwebrtc/build/android/unused_resources/BUILD.gn @@ -0,0 +1,19 @@ +# Copyright 2021 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//build/config/android/rules.gni") + +java_binary("unused_resources") { + sources = [ "//build/android/unused_resources/UnusedResources.java" ] + main_class = "build.android.unused_resources.UnusedResources" + deps = [ + "//third_party/android_deps:com_android_tools_common_java", + "//third_party/android_deps:com_android_tools_layoutlib_layoutlib_api_java", + "//third_party/android_deps:com_android_tools_sdk_common_java", + "//third_party/android_deps:com_google_guava_guava_java", + "//third_party/android_deps:org_jetbrains_kotlin_kotlin_stdlib_java", + "//third_party/r8:r8_java", + ] + wrapper_script_name = "helper/unused_resources" +} diff --git a/third_party/libwebrtc/build/android/unused_resources/UnusedResources.java b/third_party/libwebrtc/build/android/unused_resources/UnusedResources.java new file mode 100644 index 0000000000..6334223b9f --- /dev/null +++ b/third_party/libwebrtc/build/android/unused_resources/UnusedResources.java @@ -0,0 +1,594 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Modifications are owned by the Chromium Authors. +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package build.android.unused_resources; + +import static com.android.ide.common.symbols.SymbolIo.readFromAapt; +import static com.android.utils.SdkUtils.endsWithIgnoreCase; +import static com.google.common.base.Charsets.UTF_8; + +import com.android.ide.common.resources.usage.ResourceUsageModel; +import com.android.ide.common.resources.usage.ResourceUsageModel.Resource; +import com.android.ide.common.symbols.Symbol; +import com.android.ide.common.symbols.SymbolTable; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.tools.r8.CompilationFailedException; +import com.android.tools.r8.ProgramResource; +import com.android.tools.r8.ProgramResourceProvider; +import com.android.tools.r8.ResourceShrinker; +import com.android.tools.r8.ResourceShrinker.Command; +import com.android.tools.r8.ResourceShrinker.ReferenceChecker; +import com.android.tools.r8.origin.PathOrigin; +import com.android.utils.XmlUtils; +import com.google.common.base.Charsets; +import com.google.common.base.Joiner; +import com.google.common.collect.Maps; +import com.google.common.io.ByteStreams; +import com.google.common.io.Closeables; +import com.google.common.io.Files; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.xml.parsers.ParserConfigurationException; + +/** + Copied with modifications from gradle core source + https://android.googlesource.com/platform/tools/base/+/master/build-system/gradle-core/src/main/groovy/com/android/build/gradle/tasks/ResourceUsageAnalyzer.java + + Modifications are mostly to: + - Remove unused code paths to reduce complexity. + - Reduce dependencies unless absolutely required. +*/ + +public class UnusedResources { + private static final String ANDROID_RES = "android_res/"; + private static final String DOT_DEX = ".dex"; + private static final String DOT_CLASS = ".class"; + private static final String DOT_XML = ".xml"; + private static final String DOT_JAR = ".jar"; + private static final String FN_RESOURCE_TEXT = "R.txt"; + + /* A source of resource classes to track, can be either a folder or a jar */ + private final Iterable mRTxtFiles; + private final File mProguardMapping; + /** These can be class or dex files. */ + private final Iterable mClasses; + private final Iterable mManifests; + private final Iterable mResourceDirs; + + private final File mReportFile; + private final StringWriter mDebugOutput; + private final PrintWriter mDebugPrinter; + + /** The computed set of unused resources */ + private List mUnused; + + /** + * Map from resource class owners (VM format class) to corresponding resource entries. + * This lets us map back from code references (obfuscated class and possibly obfuscated field + * reference) back to the corresponding resource type and name. + */ + private Map>> mResourceObfuscation = + Maps.newHashMapWithExpectedSize(30); + + /** Obfuscated name of android/support/v7/widget/SuggestionsAdapter.java */ + private String mSuggestionsAdapter; + + /** Obfuscated name of android/support/v7/internal/widget/ResourcesWrapper.java */ + private String mResourcesWrapper; + + /* A Pair class because java does not come with batteries included. */ + private static class Pair { + private U mFirst; + private V mSecond; + + Pair(U first, V second) { + this.mFirst = first; + this.mSecond = second; + } + + public U getFirst() { + return mFirst; + } + + public V getSecond() { + return mSecond; + } + } + + public UnusedResources(Iterable rTxtFiles, Iterable classes, + Iterable manifests, File mapping, Iterable resources, File reportFile) { + mRTxtFiles = rTxtFiles; + mProguardMapping = mapping; + mClasses = classes; + mManifests = manifests; + mResourceDirs = resources; + + mReportFile = reportFile; + if (reportFile != null) { + mDebugOutput = new StringWriter(8 * 1024); + mDebugPrinter = new PrintWriter(mDebugOutput); + } else { + mDebugOutput = null; + mDebugPrinter = null; + } + } + + public void close() { + if (mDebugOutput != null) { + String output = mDebugOutput.toString(); + + if (mReportFile != null) { + File dir = mReportFile.getParentFile(); + if (dir != null) { + if ((dir.exists() || dir.mkdir()) && dir.canWrite()) { + try { + Files.asCharSink(mReportFile, Charsets.UTF_8).write(output); + } catch (IOException ignore) { + } + } + } + } + } + } + + public void analyze() throws IOException, ParserConfigurationException, SAXException { + gatherResourceValues(mRTxtFiles); + recordMapping(mProguardMapping); + + for (File jarOrDir : mClasses) { + recordClassUsages(jarOrDir); + } + recordManifestUsages(mManifests); + recordResources(mResourceDirs); + dumpReferences(); + mModel.processToolsAttributes(); + mUnused = mModel.findUnused(); + } + + public void emitConfig(Path destination) throws IOException { + File destinationFile = destination.toFile(); + if (!destinationFile.exists()) { + destinationFile.getParentFile().mkdirs(); + boolean success = destinationFile.createNewFile(); + if (!success) { + throw new IOException("Could not create " + destination); + } + } + StringBuilder sb = new StringBuilder(); + Collections.sort(mUnused); + for (Resource resource : mUnused) { + sb.append(resource.type + "/" + resource.name + "#remove\n"); + } + Files.asCharSink(destinationFile, UTF_8).write(sb.toString()); + } + + private void dumpReferences() { + if (mDebugPrinter != null) { + mDebugPrinter.print(mModel.dumpReferences()); + } + } + + private void recordResources(Iterable resources) + throws IOException, SAXException, ParserConfigurationException { + for (File resDir : resources) { + File[] resourceFolders = resDir.listFiles(); + assert resourceFolders != null : "Invalid resource directory " + resDir; + for (File folder : resourceFolders) { + ResourceFolderType folderType = ResourceFolderType.getFolderType(folder.getName()); + if (folderType != null) { + recordResources(folderType, folder); + } + } + } + } + + private void recordResources(ResourceFolderType folderType, File folder) + throws ParserConfigurationException, SAXException, IOException { + File[] files = folder.listFiles(); + if (files != null) { + for (File file : files) { + String path = file.getPath(); + mModel.file = file; + try { + boolean isXml = endsWithIgnoreCase(path, DOT_XML); + if (isXml) { + String xml = Files.toString(file, UTF_8); + Document document = XmlUtils.parseDocument(xml, true); + mModel.visitXmlDocument(file, folderType, document); + } else { + mModel.visitBinaryResource(folderType, file); + } + } finally { + mModel.file = null; + } + } + } + } + + void recordMapping(File mapping) throws IOException { + if (mapping == null || !mapping.exists()) { + return; + } + final String arrowString = " -> "; + final String resourceString = ".R$"; + Map nameMap = null; + for (String line : Files.readLines(mapping, UTF_8)) { + if (line.startsWith(" ") || line.startsWith("\t")) { + if (nameMap != null) { + // We're processing the members of a resource class: record names into the map + int n = line.length(); + int i = 0; + for (; i < n; i++) { + if (!Character.isWhitespace(line.charAt(i))) { + break; + } + } + if (i < n && line.startsWith("int", i)) { // int or int[] + int start = line.indexOf(' ', i + 3) + 1; + int arrow = line.indexOf(arrowString); + if (start > 0 && arrow != -1) { + int end = line.indexOf(' ', start + 1); + if (end != -1) { + String oldName = line.substring(start, end); + String newName = + line.substring(arrow + arrowString.length()).trim(); + if (!newName.equals(oldName)) { + nameMap.put(newName, oldName); + } + } + } + } + } + continue; + } else { + nameMap = null; + } + int index = line.indexOf(resourceString); + if (index == -1) { + // Record obfuscated names of a few known appcompat usages of + // Resources#getIdentifier that are unlikely to be used for general + // resource name reflection + if (line.startsWith("android.support.v7.widget.SuggestionsAdapter ")) { + mSuggestionsAdapter = + line.substring(line.indexOf(arrowString) + arrowString.length(), + line.indexOf(':') != -1 ? line.indexOf(':') : line.length()) + .trim() + .replace('.', '/') + + DOT_CLASS; + } else if (line.startsWith("android.support.v7.internal.widget.ResourcesWrapper ") + || line.startsWith("android.support.v7.widget.ResourcesWrapper ") + || (mResourcesWrapper == null // Recently wrapper moved + && line.startsWith( + "android.support.v7.widget.TintContextWrapper$TintResources "))) { + mResourcesWrapper = + line.substring(line.indexOf(arrowString) + arrowString.length(), + line.indexOf(':') != -1 ? line.indexOf(':') : line.length()) + .trim() + .replace('.', '/') + + DOT_CLASS; + } + continue; + } + int arrow = line.indexOf(arrowString, index + 3); + if (arrow == -1) { + continue; + } + String typeName = line.substring(index + resourceString.length(), arrow); + ResourceType type = ResourceType.fromClassName(typeName); + if (type == null) { + continue; + } + int end = line.indexOf(':', arrow + arrowString.length()); + if (end == -1) { + end = line.length(); + } + String target = line.substring(arrow + arrowString.length(), end).trim(); + String ownerName = target.replace('.', '/'); + + nameMap = Maps.newHashMap(); + Pair> pair = new Pair(type, nameMap); + mResourceObfuscation.put(ownerName, pair); + // For fast lookup in isResourceClass + mResourceObfuscation.put(ownerName + DOT_CLASS, pair); + } + } + + private void recordManifestUsages(File manifest) + throws IOException, ParserConfigurationException, SAXException { + String xml = Files.toString(manifest, UTF_8); + Document document = XmlUtils.parseDocument(xml, true); + mModel.visitXmlDocument(manifest, null, document); + } + + private void recordManifestUsages(Iterable manifests) + throws IOException, ParserConfigurationException, SAXException { + for (File manifest : manifests) { + recordManifestUsages(manifest); + } + } + + private void recordClassUsages(File file) throws IOException { + assert file.isFile(); + if (file.getPath().endsWith(DOT_DEX)) { + byte[] bytes = Files.toByteArray(file); + recordClassUsages(file, file.getName(), bytes); + } else if (file.getPath().endsWith(DOT_JAR)) { + ZipInputStream zis = null; + try { + FileInputStream fis = new FileInputStream(file); + try { + zis = new ZipInputStream(fis); + ZipEntry entry = zis.getNextEntry(); + while (entry != null) { + String name = entry.getName(); + if (name.endsWith(DOT_DEX)) { + byte[] bytes = ByteStreams.toByteArray(zis); + if (bytes != null) { + recordClassUsages(file, name, bytes); + } + } + + entry = zis.getNextEntry(); + } + } finally { + Closeables.close(fis, true); + } + } finally { + Closeables.close(zis, true); + } + } + } + + private void recordClassUsages(File file, String name, byte[] bytes) { + assert name.endsWith(DOT_DEX); + ReferenceChecker callback = new ReferenceChecker() { + @Override + public boolean shouldProcess(String internalName) { + return !isResourceClass(internalName + DOT_CLASS); + } + + @Override + public void referencedInt(int value) { + UnusedResources.this.referencedInt("dex", value, file, name); + } + + @Override + public void referencedString(String value) { + // do nothing. + } + + @Override + public void referencedStaticField(String internalName, String fieldName) { + Resource resource = getResourceFromCode(internalName, fieldName); + if (resource != null) { + ResourceUsageModel.markReachable(resource); + } + } + + @Override + public void referencedMethod( + String internalName, String methodName, String methodDescriptor) { + // Do nothing. + } + }; + ProgramResource resource = ProgramResource.fromBytes( + new PathOrigin(file.toPath()), ProgramResource.Kind.DEX, bytes, null); + ProgramResourceProvider provider = () -> Arrays.asList(resource); + try { + Command command = + (new ResourceShrinker.Builder()).addProgramResourceProvider(provider).build(); + ResourceShrinker.run(command, callback); + } catch (CompilationFailedException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + /** Returns whether the given class file name points to an aapt-generated compiled R class. */ + boolean isResourceClass(String name) { + if (mResourceObfuscation.containsKey(name)) { + return true; + } + int index = name.lastIndexOf('/'); + if (index != -1 && name.startsWith("R$", index + 1) && name.endsWith(DOT_CLASS)) { + String typeName = name.substring(index + 3, name.length() - DOT_CLASS.length()); + return ResourceType.fromClassName(typeName) != null; + } + return false; + } + + Resource getResourceFromCode(String owner, String name) { + Pair> pair = mResourceObfuscation.get(owner); + if (pair != null) { + ResourceType type = pair.getFirst(); + Map nameMap = pair.getSecond(); + String renamedField = nameMap.get(name); + if (renamedField != null) { + name = renamedField; + } + return mModel.getResource(type, name); + } + if (isValidResourceType(owner)) { + ResourceType type = + ResourceType.fromClassName(owner.substring(owner.lastIndexOf('$') + 1)); + if (type != null) { + return mModel.getResource(type, name); + } + } + return null; + } + + private Boolean isValidResourceType(String candidateString) { + return candidateString.contains("/") + && candidateString.substring(candidateString.lastIndexOf('/') + 1).contains("$"); + } + + private void gatherResourceValues(Iterable rTxts) throws IOException { + for (File rTxt : rTxts) { + assert rTxt.isFile(); + assert rTxt.getName().endsWith(FN_RESOURCE_TEXT); + addResourcesFromRTxtFile(rTxt); + } + } + + private void addResourcesFromRTxtFile(File file) { + try { + SymbolTable st = readFromAapt(file, null); + for (Symbol symbol : st.getSymbols().values()) { + String symbolValue = symbol.getValue(); + if (symbol.getResourceType() == ResourceType.STYLEABLE) { + if (symbolValue.trim().startsWith("{")) { + // Only add the styleable parent, styleable children are not yet supported. + mModel.addResource(symbol.getResourceType(), symbol.getName(), null); + } + } else { + mModel.addResource(symbol.getResourceType(), symbol.getName(), symbolValue); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + ResourceUsageModel getModel() { + return mModel; + } + + private void referencedInt(String context, int value, File file, String currentClass) { + Resource resource = mModel.getResource(value); + if (ResourceUsageModel.markReachable(resource) && mDebugPrinter != null) { + mDebugPrinter.println("Marking " + resource + " reachable: referenced from " + context + + " in " + file + ":" + currentClass); + } + } + + private final ResourceShrinkerUsageModel mModel = new ResourceShrinkerUsageModel(); + + private class ResourceShrinkerUsageModel extends ResourceUsageModel { + public File file; + + /** + * Whether we should ignore tools attribute resource references. + *

+ * For example, for resource shrinking we want to ignore tools attributes, + * whereas for resource refactoring on the source code we do not. + * + * @return whether tools attributes should be ignored + */ + @Override + protected boolean ignoreToolsAttributes() { + return true; + } + + @Override + protected void onRootResourcesFound(List roots) { + if (mDebugPrinter != null) { + mDebugPrinter.println( + "\nThe root reachable resources are:\n" + Joiner.on(",\n ").join(roots)); + } + } + + @Override + protected Resource declareResource(ResourceType type, String name, Node node) { + Resource resource = super.declareResource(type, name, node); + resource.addLocation(file); + return resource; + } + + @Override + protected void referencedString(String string) { + // Do nothing + } + } + + public static void main(String[] args) throws Exception { + List rTxtFiles = null; // R.txt files + List classes = null; // Dex/jar w dex + List manifests = null; // manifests + File mapping = null; // mapping + List resources = null; // resources dirs + File log = null; // output log for debugging + Path configPath = null; // output config + for (int i = 0; i < args.length; i += 2) { + switch (args[i]) { + case "--rtxts": + rTxtFiles = Arrays.stream(args[i + 1].split(":")) + .map(s -> new File(s)) + .collect(Collectors.toList()); + break; + case "--dexes": + classes = Arrays.stream(args[i + 1].split(":")) + .map(s -> new File(s)) + .collect(Collectors.toList()); + break; + case "--manifests": + manifests = Arrays.stream(args[i + 1].split(":")) + .map(s -> new File(s)) + .collect(Collectors.toList()); + break; + case "--mapping": + mapping = new File(args[i + 1]); + break; + case "--resourceDirs": + resources = Arrays.stream(args[i + 1].split(":")) + .map(s -> new File(s)) + .collect(Collectors.toList()); + break; + case "--log": + log = new File(args[i + 1]); + break; + case "--outputConfig": + configPath = Paths.get(args[i + 1]); + break; + default: + throw new IllegalArgumentException(args[i] + " is not a valid arg."); + } + } + UnusedResources unusedResources = + new UnusedResources(rTxtFiles, classes, manifests, mapping, resources, log); + unusedResources.analyze(); + unusedResources.close(); + unusedResources.emitConfig(configPath); + } +} -- cgit v1.2.3