[email protected] | b98ea60 | 2013-08-29 21:07:50 | [diff] [blame] | 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__ = '[email protected] (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', |
[email protected] | 5974f03d | 2013-09-03 18:13:05 | [diff] [blame] | 41 | '_LEFT', '_CENTER', '_RIGHT', |
[email protected] | b98ea60 | 2013-08-29 21:07:50 | [diff] [blame] | 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. |
[email protected] | fd061c5d | 2014-01-11 23:19:17 | [diff] [blame] | 101 | searched = set() |
[email protected] | b98ea60 | 2013-08-29 21:07:50 | [diff] [blame] | 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) |
[email protected] | fd061c5d | 2014-01-11 23:19:17 | [diff] [blame] | 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 | |
[email protected] | b98ea60 | 2013-08-29 21:07:50 | [diff] [blame] | 115 | # Print progress as we go along. |
| 116 | print resource_id |
[email protected] | fd061c5d | 2014-01-11 23:19:17 | [diff] [blame] | 117 | |
[email protected] | b98ea60 | 2013-08-29 21:07:50 | [diff] [blame] | 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.). |
[email protected] | b98ea60 | 2013-08-29 21:07:50 | [diff] [blame] | 121 | matching_files = FindFilesWithContents(base_resource_id, filename) |
[email protected] | fd061c5d | 2014-01-11 23:19:17 | [diff] [blame] | 122 | |
[email protected] | b98ea60 | 2013-08-29 21:07:50 | [diff] [blame] | 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() |