cmConvertMSBuildXMLToJSON.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. # Distributed under the OSI-approved BSD 3-Clause License. See accompanying
  2. # file Copyright.txt or https://cmake.org/licensing for details.
  3. import argparse
  4. import codecs
  5. import copy
  6. import logging
  7. import json
  8. import os
  9. from collections import OrderedDict
  10. from xml.dom.minidom import parse, parseString, Element
  11. class VSFlags:
  12. """Flags corresponding to cmIDEFlagTable."""
  13. UserValue = "UserValue" # (1 << 0)
  14. UserIgnored = "UserIgnored" # (1 << 1)
  15. UserRequired = "UserRequired" # (1 << 2)
  16. Continue = "Continue" #(1 << 3)
  17. SemicolonAppendable = "SemicolonAppendable" # (1 << 4)
  18. UserFollowing = "UserFollowing" # (1 << 5)
  19. CaseInsensitive = "CaseInsensitive" # (1 << 6)
  20. UserValueIgnored = [UserValue, UserIgnored]
  21. UserValueRequired = [UserValue, UserRequired]
  22. def vsflags(*args):
  23. """Combines the flags."""
  24. values = []
  25. for arg in args:
  26. __append_list(values, arg)
  27. return values
  28. def read_msbuild_xml(path, values=None):
  29. """Reads the MS Build XML file at the path and returns its contents.
  30. Keyword arguments:
  31. values -- The map to append the contents to (default {})
  32. """
  33. if values is None:
  34. values = {}
  35. # Attempt to read the file contents
  36. try:
  37. document = parse(path)
  38. except Exception as e:
  39. logging.exception('Could not read MS Build XML file at %s', path)
  40. return values
  41. # Convert the XML to JSON format
  42. logging.info('Processing MS Build XML file at %s', path)
  43. # Get the rule node
  44. rule = document.getElementsByTagName('Rule')[0]
  45. rule_name = rule.attributes['Name'].value
  46. logging.info('Found rules for %s', rule_name)
  47. # Proprocess Argument values
  48. __preprocess_arguments(rule)
  49. # Get all the values
  50. converted_values = []
  51. __convert(rule, 'EnumProperty', converted_values, __convert_enum)
  52. __convert(rule, 'BoolProperty', converted_values, __convert_bool)
  53. __convert(rule, 'StringListProperty', converted_values,
  54. __convert_string_list)
  55. __convert(rule, 'StringProperty', converted_values, __convert_string)
  56. __convert(rule, 'IntProperty', converted_values, __convert_string)
  57. values[rule_name] = converted_values
  58. return values
  59. def read_msbuild_json(path, values=None):
  60. """Reads the MS Build JSON file at the path and returns its contents.
  61. Keyword arguments:
  62. values -- The list to append the contents to (default [])
  63. """
  64. if values is None:
  65. values = []
  66. if not os.path.exists(path):
  67. logging.info('Could not find MS Build JSON file at %s', path)
  68. return values
  69. try:
  70. values.extend(__read_json_file(path))
  71. except Exception as e:
  72. logging.exception('Could not read MS Build JSON file at %s', path)
  73. return values
  74. logging.info('Processing MS Build JSON file at %s', path)
  75. return values
  76. def main():
  77. """Script entrypoint."""
  78. # Parse the arguments
  79. parser = argparse.ArgumentParser(
  80. description='Convert MSBuild XML to JSON format')
  81. parser.add_argument(
  82. '-t', '--toolchain', help='The name of the toolchain', required=True)
  83. parser.add_argument(
  84. '-o', '--output', help='The output directory', default='')
  85. parser.add_argument(
  86. '-r',
  87. '--overwrite',
  88. help='Whether previously output should be overwritten',
  89. dest='overwrite',
  90. action='store_true')
  91. parser.set_defaults(overwrite=False)
  92. parser.add_argument(
  93. '-d',
  94. '--debug',
  95. help="Debug tool output",
  96. action="store_const",
  97. dest="loglevel",
  98. const=logging.DEBUG,
  99. default=logging.WARNING)
  100. parser.add_argument(
  101. '-v',
  102. '--verbose',
  103. help="Verbose output",
  104. action="store_const",
  105. dest="loglevel",
  106. const=logging.INFO)
  107. parser.add_argument('input', help='The input files', nargs='+')
  108. args = parser.parse_args()
  109. toolchain = args.toolchain
  110. logging.basicConfig(level=args.loglevel)
  111. logging.info('Creating %s toolchain files', toolchain)
  112. values = {}
  113. # Iterate through the inputs
  114. for input in args.input:
  115. input = __get_path(input)
  116. read_msbuild_xml(input, values)
  117. # Determine if the output directory needs to be created
  118. output_dir = __get_path(args.output)
  119. if not os.path.exists(output_dir):
  120. os.mkdir(output_dir)
  121. logging.info('Created output directory %s', output_dir)
  122. for key, value in values.items():
  123. output_path = __output_path(toolchain, key, output_dir)
  124. if os.path.exists(output_path) and not args.overwrite:
  125. logging.info('Comparing previous output to current')
  126. __merge_json_values(value, read_msbuild_json(output_path))
  127. else:
  128. logging.info('Original output will be overwritten')
  129. logging.info('Writing MS Build JSON file at %s', output_path)
  130. __write_json_file(output_path, value)
  131. ###########################################################################################
  132. # private joining functions
  133. def __merge_json_values(current, previous):
  134. """Merges the values between the current and previous run of the script."""
  135. for value in current:
  136. name = value['name']
  137. # Find the previous value
  138. previous_value = __find_and_remove_value(previous, value)
  139. if previous_value is not None:
  140. flags = value['flags']
  141. previous_flags = previous_value['flags']
  142. if flags != previous_flags:
  143. logging.warning(
  144. 'Flags for %s are different. Using previous value.', name)
  145. value['flags'] = previous_flags
  146. else:
  147. logging.warning('Value %s is a new value', name)
  148. for value in previous:
  149. name = value['name']
  150. logging.warning(
  151. 'Value %s not present in current run. Appending value.', name)
  152. current.append(value)
  153. def __find_and_remove_value(list, compare):
  154. """Finds the value in the list that corresponds with the value of compare."""
  155. # next throws if there are no matches
  156. try:
  157. found = next(value for value in list
  158. if value['name'] == compare['name'] and value['switch'] ==
  159. compare['switch'])
  160. except:
  161. return None
  162. list.remove(found)
  163. return found
  164. def __normalize_switch(switch, separator):
  165. new = switch
  166. if switch.startswith("/") or switch.startswith("-"):
  167. new = switch[1:]
  168. if new and separator:
  169. new = new + separator
  170. return new
  171. ###########################################################################################
  172. # private xml functions
  173. def __convert(root, tag, values, func):
  174. """Converts the tag type found in the root and converts them using the func
  175. and appends them to the values.
  176. """
  177. elements = root.getElementsByTagName(tag)
  178. for element in elements:
  179. converted = func(element)
  180. # Append to the list
  181. __append_list(values, converted)
  182. def __convert_enum(node):
  183. """Converts an EnumProperty node to JSON format."""
  184. name = __get_attribute(node, 'Name')
  185. logging.debug('Found EnumProperty named %s', name)
  186. converted_values = []
  187. for value in node.getElementsByTagName('EnumValue'):
  188. converted = __convert_node(value)
  189. converted['value'] = converted['name']
  190. converted['name'] = name
  191. # Modify flags when there is an argument child
  192. __with_argument(value, converted)
  193. converted_values.append(converted)
  194. return converted_values
  195. def __convert_bool(node):
  196. """Converts an BoolProperty node to JSON format."""
  197. converted = __convert_node(node, default_value='true')
  198. # Check for a switch for reversing the value
  199. reverse_switch = __get_attribute(node, 'ReverseSwitch')
  200. if reverse_switch:
  201. __with_argument(node, converted)
  202. converted_reverse = copy.deepcopy(converted)
  203. converted_reverse['switch'] = reverse_switch
  204. converted_reverse['value'] = 'false'
  205. return [converted_reverse, converted]
  206. # Modify flags when there is an argument child
  207. __with_argument(node, converted)
  208. return __check_for_flag(converted)
  209. def __convert_string_list(node):
  210. """Converts a StringListProperty node to JSON format."""
  211. converted = __convert_node(node)
  212. # Determine flags for the string list
  213. flags = vsflags(VSFlags.UserValue)
  214. # Check for a separator to determine if it is semicolon appendable
  215. # If not present assume the value should be ;
  216. separator = __get_attribute(node, 'Separator', default_value=';')
  217. if separator == ';':
  218. flags = vsflags(flags, VSFlags.SemicolonAppendable)
  219. converted['flags'] = flags
  220. return __check_for_flag(converted)
  221. def __convert_string(node):
  222. """Converts a StringProperty node to JSON format."""
  223. converted = __convert_node(node, default_flags=vsflags(VSFlags.UserValue))
  224. return __check_for_flag(converted)
  225. def __convert_node(node, default_value='', default_flags=vsflags()):
  226. """Converts a XML node to a JSON equivalent."""
  227. name = __get_attribute(node, 'Name')
  228. logging.debug('Found %s named %s', node.tagName, name)
  229. converted = {}
  230. converted['name'] = name
  231. switch = __get_attribute(node, 'Switch')
  232. separator = __get_attribute(node, 'Separator')
  233. converted['switch'] = __normalize_switch(switch, separator)
  234. converted['comment'] = __get_attribute(node, 'DisplayName')
  235. converted['value'] = default_value
  236. # Check for the Flags attribute in case it was created during preprocessing
  237. flags = __get_attribute(node, 'Flags')
  238. if flags:
  239. flags = flags.split(',')
  240. else:
  241. flags = default_flags
  242. converted['flags'] = flags
  243. return converted
  244. def __check_for_flag(value):
  245. """Checks whether the value has a switch value.
  246. If not then returns None as it should not be added.
  247. """
  248. if value['switch']:
  249. return value
  250. else:
  251. logging.warning('Skipping %s which has no command line switch',
  252. value['name'])
  253. return None
  254. def __with_argument(node, value):
  255. """Modifies the flags in value if the node contains an Argument."""
  256. arguments = node.getElementsByTagName('Argument')
  257. if arguments:
  258. logging.debug('Found argument within %s', value['name'])
  259. value['flags'] = vsflags(VSFlags.UserValueIgnored, VSFlags.Continue)
  260. def __preprocess_arguments(root):
  261. """Preprocesses occurrences of Argument within the root.
  262. Argument XML values reference other values within the document by name. The
  263. referenced value does not contain a switch. This function will add the
  264. switch associated with the argument.
  265. """
  266. # Set the flags to require a value
  267. flags = ','.join(vsflags(VSFlags.UserValueRequired))
  268. # Search through the arguments
  269. arguments = root.getElementsByTagName('Argument')
  270. for argument in arguments:
  271. reference = __get_attribute(argument, 'Property')
  272. found = None
  273. # Look for the argument within the root's children
  274. for child in root.childNodes:
  275. # Ignore Text nodes
  276. if isinstance(child, Element):
  277. name = __get_attribute(child, 'Name')
  278. if name == reference:
  279. found = child
  280. break
  281. if found is not None:
  282. logging.info('Found property named %s', reference)
  283. # Get the associated switch
  284. switch = __get_attribute(argument.parentNode, 'Switch')
  285. # See if there is already a switch associated with the element.
  286. if __get_attribute(found, 'Switch'):
  287. logging.debug('Copying node %s', reference)
  288. clone = found.cloneNode(True)
  289. root.insertBefore(clone, found)
  290. found = clone
  291. found.setAttribute('Switch', switch)
  292. found.setAttribute('Flags', flags)
  293. else:
  294. logging.warning('Could not find property named %s', reference)
  295. def __get_attribute(node, name, default_value=''):
  296. """Retrieves the attribute of the given name from the node.
  297. If not present then the default_value is used.
  298. """
  299. if node.hasAttribute(name):
  300. return node.attributes[name].value.strip()
  301. else:
  302. return default_value
  303. ###########################################################################################
  304. # private path functions
  305. def __get_path(path):
  306. """Gets the path to the file."""
  307. if not os.path.isabs(path):
  308. path = os.path.join(os.getcwd(), path)
  309. return os.path.normpath(path)
  310. def __output_path(toolchain, rule, output_dir):
  311. """Gets the output path for a file given the toolchain, rule and output_dir"""
  312. filename = '%s_%s.json' % (toolchain, rule)
  313. return os.path.join(output_dir, filename)
  314. ###########################################################################################
  315. # private JSON file functions
  316. def __read_json_file(path):
  317. """Reads a JSON file at the path."""
  318. with open(path, 'r') as f:
  319. return json.load(f)
  320. def __write_json_file(path, values):
  321. """Writes a JSON file at the path with the values provided."""
  322. # Sort the keys to ensure ordering
  323. sort_order = ['name', 'switch', 'comment', 'value', 'flags']
  324. sorted_values = [
  325. OrderedDict(
  326. sorted(
  327. value.items(), key=lambda value: sort_order.index(value[0])))
  328. for value in values
  329. ]
  330. with open(path, 'w') as f:
  331. json.dump(sorted_values, f, indent=2, separators=(',', ': '))
  332. f.write("\n")
  333. ###########################################################################################
  334. # private list helpers
  335. def __append_list(append_to, value):
  336. """Appends the value to the list."""
  337. if value is not None:
  338. if isinstance(value, list):
  339. append_to.extend(value)
  340. else:
  341. append_to.append(value)
  342. ###########################################################################################
  343. # main entry point
  344. if __name__ == "__main__":
  345. main()