generate_ui_string_overrider.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. #!/usr/bin/env python
  2. # Copyright 2014 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. import argparse
  6. import collections
  7. import hashlib
  8. import operator
  9. import os
  10. import re
  11. import sys
  12. SCRIPT_NAME = "generate_ui_string_overrider.py"
  13. # Regular expression for parsing the #define macro format. Matches both the
  14. # version of the macro with allowlist support and the one without. For example,
  15. # Without generate allowlist flag:
  16. # #define IDS_FOO_MESSAGE 1234
  17. # With generate allowlist flag:
  18. # #define IDS_FOO_MESSAGE (::ui::AllowlistedResource<1234>(), 1234)
  19. RESOURCE_EXTRACT_REGEX = re.compile('^#define (\S*).* (\d+)\)?$', re.MULTILINE)
  20. class Error(Exception):
  21. """Base error class for all exceptions in generated_resources_map."""
  22. class HashCollisionError(Error):
  23. """Multiple resource names hash to the same value."""
  24. Resource = collections.namedtuple("Resource", ['hash', 'name', 'index'])
  25. def HashName(name):
  26. """Returns the hash id for a name.
  27. Args:
  28. name: The name to hash.
  29. Returns:
  30. An int that is at most 32 bits.
  31. """
  32. md5hash = hashlib.md5()
  33. md5hash.update(name.encode('utf-8'))
  34. return int(md5hash.hexdigest()[:8], 16)
  35. def _GetNameIndexPairsIter(string_to_scan):
  36. """Gets an iterator of the resource name and index pairs of the given string.
  37. Scans the input string for lines of the form "#define NAME INDEX" and returns
  38. an iterator over all matching (NAME, INDEX) pairs.
  39. Args:
  40. string_to_scan: The input string to scan.
  41. Yields:
  42. A tuple of name and index.
  43. """
  44. for match in RESOURCE_EXTRACT_REGEX.finditer(string_to_scan):
  45. yield match.group(1, 2)
  46. def _GetResourceListFromString(resources_content):
  47. """Produces a list of |Resource| objects from a string.
  48. The input string contains lines of the form "#define NAME INDEX". The returned
  49. list is sorted primarily by hash, then name, and then index.
  50. Args:
  51. resources_content: The input string to process, contains lines of the form
  52. "#define NAME INDEX".
  53. Returns:
  54. A sorted list of |Resource| objects.
  55. """
  56. resources = [Resource(HashName(name), name, index) for name, index in
  57. _GetNameIndexPairsIter(resources_content)]
  58. # Deduplicate resources. Some name-index pairs appear in both chromium_ and
  59. # google_chrome_ header files. Unless deduplicated here, collisions will be
  60. # raised in _CheckForHashCollisions.
  61. resources = list(set(resources))
  62. # The default |Resource| order makes |resources| sorted by the hash, then
  63. # name, then index.
  64. resources.sort()
  65. return resources
  66. def _CheckForHashCollisions(sorted_resource_list):
  67. """Checks a sorted list of |Resource| objects for hash collisions.
  68. Args:
  69. sorted_resource_list: A sorted list of |Resource| objects.
  70. Returns:
  71. A set of all |Resource| objects with collisions.
  72. """
  73. collisions = set()
  74. for i in range(len(sorted_resource_list) - 1):
  75. resource = sorted_resource_list[i]
  76. next_resource = sorted_resource_list[i+1]
  77. if resource.hash == next_resource.hash:
  78. collisions.add(resource)
  79. collisions.add(next_resource)
  80. return collisions
  81. def _GenDataArray(
  82. resources, entry_pattern, array_name, array_type, data_getter):
  83. """Generates a C++ statement defining a literal array containing the hashes.
  84. Args:
  85. resources: A sorted list of |Resource| objects.
  86. entry_pattern: A pattern to be used to generate each entry in the array. The
  87. pattern is expected to have a place for data and one for a comment, in
  88. that order.
  89. array_name: The name of the array being generated.
  90. array_type: The type of the array being generated.
  91. data_getter: A function that gets the array data from a |Resource| object.
  92. Returns:
  93. A string containing a C++ statement defining the an array.
  94. """
  95. lines = [entry_pattern % (data_getter(r), r.name) for r in resources]
  96. pattern = """const %(type)s %(name)s[] = {
  97. %(content)s
  98. };
  99. """
  100. return pattern % {'type': array_type,
  101. 'name': array_name,
  102. 'content': '\n'.join(lines)}
  103. def _GenerateNamespacePrefixAndSuffix(namespace):
  104. """Generates the namespace prefix and suffix for |namespace|.
  105. Args:
  106. namespace: A string corresponding to the namespace name. May be empty.
  107. Returns:
  108. A tuple of strings corresponding to the namespace prefix and suffix for
  109. putting the code in the corresponding namespace in C++. If namespace is
  110. the empty string, both returned strings are empty too.
  111. """
  112. if not namespace:
  113. return "", ""
  114. return "namespace %s {\n\n" % namespace, "\n} // namespace %s\n" % namespace
  115. def _GenerateSourceFileContent(resources_content, namespace, header_filename):
  116. """Generates the .cc content from the given generated grit headers content.
  117. Args:
  118. resources_content: The input string to process, contains lines of the form
  119. "#define NAME INDEX".
  120. namespace: The namespace in which the generated code should be scoped. If
  121. not defined, then the code will be in the global namespace.
  122. header_filename: Path to the corresponding .h.
  123. Returns:
  124. .cc file content implementing the CreateUIStringOverrider() factory.
  125. """
  126. hashed_tuples = _GetResourceListFromString(resources_content)
  127. collisions = _CheckForHashCollisions(hashed_tuples)
  128. if collisions:
  129. error_message = "\n".join(
  130. ["hash: %i, name: %s" % (i.hash, i.name) for i in sorted(collisions)])
  131. error_message = ("\nThe following names, sorted by hash value, "
  132. "had hash collisions (One possible cause: strings "
  133. "appear in different orders for Chrome and Chromium):"
  134. "\n%s\n" % (error_message))
  135. raise HashCollisionError(error_message)
  136. hashes_array = _GenDataArray(
  137. hashed_tuples, " %iU, // %s", 'kResourceHashes', 'uint32_t',
  138. operator.attrgetter('hash'))
  139. indices_array = _GenDataArray(
  140. hashed_tuples, " %s, // %s", 'kResourceIndices', 'int',
  141. operator.attrgetter('index'))
  142. namespace_prefix, namespace_suffix = _GenerateNamespacePrefixAndSuffix(
  143. namespace)
  144. return (
  145. "// This file was generated by %(script_name)s. Do not edit.\n"
  146. "\n"
  147. "#include \"%(header_filename)s\"\n\n"
  148. "%(namespace_prefix)s"
  149. "namespace {\n\n"
  150. "const size_t kNumResources = %(num_resources)i;\n\n"
  151. "%(hashes_array)s"
  152. "\n"
  153. "%(indices_array)s"
  154. "\n"
  155. "} // namespace\n"
  156. "\n"
  157. "variations::UIStringOverrider CreateUIStringOverrider() {\n"
  158. " return variations::UIStringOverrider(\n"
  159. " kResourceHashes, kResourceIndices, kNumResources);\n"
  160. "}\n"
  161. "%(namespace_suffix)s") % {
  162. 'script_name': SCRIPT_NAME,
  163. 'header_filename': header_filename,
  164. 'namespace_prefix': namespace_prefix,
  165. 'num_resources': len(hashed_tuples),
  166. 'hashes_array': hashes_array,
  167. 'indices_array': indices_array,
  168. 'namespace_suffix': namespace_suffix,
  169. }
  170. def _GenerateHeaderFileContent(namespace, header_filename):
  171. """Generates the .h for to the .cc generated by _GenerateSourceFileContent.
  172. Args:
  173. namespace: The namespace in which the generated code should be scoped. If
  174. not defined, then the code will be in the global namespace.
  175. header_filename: Path to the corresponding .h. Used to generate the include
  176. guards.
  177. Returns:
  178. .cc file content implementing the CreateUIStringOverrider() factory.
  179. """
  180. include_guard = re.sub('[^A-Z]', '_', header_filename.upper()) + '_'
  181. namespace_prefix, namespace_suffix = _GenerateNamespacePrefixAndSuffix(
  182. namespace)
  183. return (
  184. "// This file was generated by %(script_name)s. Do not edit.\n"
  185. "\n"
  186. "#ifndef %(include_guard)s\n"
  187. "#define %(include_guard)s\n"
  188. "\n"
  189. "#include \"components/variations/service/ui_string_overrider.h\"\n\n"
  190. "%(namespace_prefix)s"
  191. "// Returns an initialized UIStringOverrider.\n"
  192. "variations::UIStringOverrider CreateUIStringOverrider();\n"
  193. "%(namespace_suffix)s"
  194. "\n"
  195. "#endif // %(include_guard)s\n"
  196. ) % {
  197. 'script_name': SCRIPT_NAME,
  198. 'include_guard': include_guard,
  199. 'namespace_prefix': namespace_prefix,
  200. 'namespace_suffix': namespace_suffix,
  201. }
  202. def main():
  203. arg_parser = argparse.ArgumentParser(
  204. description="Generate UIStringOverrider factory from resources headers "
  205. "generated by grit.")
  206. arg_parser.add_argument(
  207. "--output_dir", "-o", required=True,
  208. help="Base directory to for generated files.")
  209. arg_parser.add_argument(
  210. "--source_filename", "-S", required=True,
  211. help="File name of the generated source file.")
  212. arg_parser.add_argument(
  213. "--header_filename", "-H", required=True,
  214. help="File name of the generated header file.")
  215. arg_parser.add_argument(
  216. "--namespace", "-N", default="",
  217. help="Namespace of the generated factory function (code will be in "
  218. "the global namespace if this is omitted).")
  219. arg_parser.add_argument(
  220. "--test_support", "-t", action="store_true", default=False,
  221. help="Make internal variables accessible for testing.")
  222. arg_parser.add_argument(
  223. "inputs", metavar="FILENAME", nargs="+",
  224. help="Path to resources header file generated by grit.")
  225. arguments = arg_parser.parse_args()
  226. generated_resources_h = ""
  227. for resources_file in arguments.inputs:
  228. with open(resources_file, "r") as resources:
  229. generated_resources_h += resources.read()
  230. if len(generated_resources_h) == 0:
  231. raise Error("No content loaded for %s." % (resources_file))
  232. source_file_content = _GenerateSourceFileContent(
  233. generated_resources_h, arguments.namespace, arguments.header_filename)
  234. header_file_content = _GenerateHeaderFileContent(
  235. arguments.namespace, arguments.header_filename)
  236. with open(os.path.join(
  237. arguments.output_dir, arguments.source_filename), "w") as generated_file:
  238. generated_file.write(source_file_content)
  239. with open(os.path.join(
  240. arguments.output_dir, arguments.header_filename), "w") as generated_file:
  241. generated_file.write(header_file_content)
  242. if __name__ == '__main__':
  243. sys.exit(main())