1 #!/usr/bin/env python 2 # Copyright 2013 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 """This script searches for unused art assets listed in a .grd file. 7 8 It uses git grep to look for references to the IDR resource id or the base 9 filename. If neither is found, the file is reported unused. 10 11 Requires a git checkout. Must be run from your checkout's "src" root. 12 13 Example: 14 cd /work/chrome/src 15 tools/resources/find_unused_resouces.py ash/resources/ash_resources.grd 16 """ 17 18 __author__ = 'jamescook (at] chromium.org (James Cook)' 19 20 21 import os 22 import re 23 import subprocess 24 import sys 25 26 27 def GetBaseResourceId(resource_id): 28 """Removes common suffixes from a resource ID. 29 30 Removes suffixies that may be added by macros like IMAGE_GRID or IMAGE_BORDER. 31 For example, converts IDR_FOO_LEFT and IDR_FOO_RIGHT to just IDR_FOO. 32 33 Args: 34 resource_id: String resource ID. 35 36 Returns: 37 A string with the base part of the resource ID. 38 """ 39 suffixes = [ 40 '_TOP_LEFT', '_TOP', '_TOP_RIGHT', 41 '_LEFT', '_CENTER', '_RIGHT', 42 '_BOTTOM_LEFT', '_BOTTOM', '_BOTTOM_RIGHT', 43 '_TL', '_T', '_TR', 44 '_L', '_M', '_R', 45 '_BL', '_B', '_BR'] 46 # Note: This does not check _HOVER, _PRESSED, _HOT, etc. as those are never 47 # used in macros. 48 for suffix in suffixes: 49 if resource_id.endswith(suffix): 50 resource_id = resource_id[:-len(suffix)] 51 return resource_id 52 53 54 def FindFilesWithContents(string_a, string_b): 55 """Returns list of paths of files that contain |string_a| or |string_b|. 56 57 Uses --name-only to print the file paths. The default behavior of git grep 58 is to OR together multiple patterns. 59 60 Args: 61 string_a: A string to search for (not a regular expression). 62 string_b: As above. 63 64 Returns: 65 A list of file paths as strings. 66 """ 67 matching_files = subprocess.check_output([ 68 'git', 'grep', '--name-only', '--fixed-strings', '-e', string_a, 69 '-e', string_b]) 70 files_list = matching_files.split('\n') 71 # The output ends in a newline, so slice that off. 72 files_list = files_list[:-1] 73 return files_list 74 75 76 def GetUnusedResources(grd_filepath): 77 """Returns a list of resources that are unused in the code. 78 79 Prints status lines to the console because this function is quite slow. 80 81 Args: 82 grd_filepath: Path to a .grd file listing resources. 83 84 Returns: 85 A list of pairs of [resource_id, filepath] for the unused resources. 86 """ 87 unused_resources = [] 88 grd_file = open(grd_filepath, 'r') 89 grd_data = grd_file.read() 90 print 'Checking:' 91 # Match the resource id and file path out of substrings like: 92 # ...name="IDR_FOO_123" file="common/foo.png"... 93 # by matching between the quotation marks. 94 pattern = re.compile( 95 r"""name="([^"]*)" # Match resource ID between quotes. 96 \s* # Run of whitespace, including newlines. 97 file="([^"]*)" # Match file path between quotes.""", 98 re.VERBOSE) 99 # Use finditer over the file contents because there may be newlines between 100 # the name and file attributes. 101 searched = set() 102 for result in pattern.finditer(grd_data): 103 # Extract the IDR resource id and file path. 104 resource_id = result.group(1) 105 filepath = result.group(2) 106 filename = os.path.basename(filepath) 107 base_resource_id = GetBaseResourceId(resource_id) 108 109 # Do not bother repeating searches. 110 key = (base_resource_id, filename) 111 if key in searched: 112 continue 113 searched.add(key) 114 115 # Print progress as we go along. 116 print resource_id 117 118 # Ensure the resource isn't used anywhere by checking both for the resource 119 # id (which should appear in C++ code) and the raw filename (in case the 120 # file is referenced in a script, test HTML file, etc.). 121 matching_files = FindFilesWithContents(base_resource_id, filename) 122 123 # Each file is matched once in the resource file itself. If there are no 124 # other matching files, it is unused. 125 if len(matching_files) == 1: 126 # Give the user some happy news. 127 print 'Unused!' 128 unused_resources.append([resource_id, filepath]) 129 130 return unused_resources 131 132 133 def GetScaleDirectories(resources_path): 134 """Returns a list of paths to per-scale-factor resource directories. 135 136 Assumes the directory names end in '_percent', for example, 137 ash/resources/default_200_percent or 138 chrome/app/theme/resources/touch_140_percent 139 140 Args: 141 resources_path: The base path of interest. 142 143 Returns: 144 A list of paths relative to the 'src' directory. 145 """ 146 file_list = os.listdir(resources_path) 147 scale_directories = [] 148 for file_entry in file_list: 149 file_path = os.path.join(resources_path, file_entry) 150 if os.path.isdir(file_path) and file_path.endswith('_percent'): 151 scale_directories.append(file_path) 152 153 scale_directories.sort() 154 return scale_directories 155 156 157 def main(): 158 # The script requires exactly one parameter, the .grd file path. 159 if len(sys.argv) != 2: 160 print 'Usage: tools/resources/find_unused_resources.py <path/to/grd>' 161 sys.exit(1) 162 grd_filepath = sys.argv[1] 163 164 # Try to ensure we are in a source checkout. 165 current_dir = os.getcwd() 166 if os.path.basename(current_dir) != 'src': 167 print 'Script must be run in your "src" directory.' 168 sys.exit(1) 169 170 # We require a git checkout to use git grep. 171 if not os.path.exists(current_dir + '/.git'): 172 print 'You must use a git checkout for this script to run.' 173 print current_dir + '/.git', 'not found.' 174 sys.exit(1) 175 176 # Look up the scale-factor directories. 177 resources_path = os.path.dirname(grd_filepath) 178 scale_directories = GetScaleDirectories(resources_path) 179 if not scale_directories: 180 print 'No scale directories (like "default_100_percent") found.' 181 sys.exit(1) 182 183 # |unused_resources| stores pairs of [resource_id, filepath] for resource ids 184 # that are not referenced in the code. 185 unused_resources = GetUnusedResources(grd_filepath) 186 if not unused_resources: 187 print 'All resources are used.' 188 sys.exit(0) 189 190 # Dump our output for the user. 191 print 192 print 'Unused resource ids:' 193 for resource_id, filepath in unused_resources: 194 print resource_id 195 # Print a list of 'git rm' command lines to remove unused assets. 196 print 197 print 'Unused files:' 198 for resource_id, filepath in unused_resources: 199 for directory in scale_directories: 200 print 'git rm ' + os.path.join(directory, filepath) 201 202 203 if __name__ == '__main__': 204 main() 205