find_unused_resources.py 6.4 KB

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