cmConvertMSBuildXMLToJSON.py 13 KB

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