1 # Distributed under the OSI-approved BSD 3-Clause License. See accompanying
2 # file Copyright.txt or https://cmake.org/licensing for details.
11 from collections import OrderedDict
12 from xml.dom.minidom import parse, parseString, Element
16 """Flags corresponding to cmIDEFlagTable."""
17 UserValue = "UserValue" # (1 << 0)
18 UserIgnored = "UserIgnored" # (1 << 1)
19 UserRequired = "UserRequired" # (1 << 2)
20 Continue = "Continue" #(1 << 3)
21 SemicolonAppendable = "SemicolonAppendable" # (1 << 4)
22 UserFollowing = "UserFollowing" # (1 << 5)
23 CaseInsensitive = "CaseInsensitive" # (1 << 6)
24 UserValueIgnored = [UserValue, UserIgnored]
25 UserValueRequired = [UserValue, UserRequired]
29 """Combines the flags."""
33 __append_list(values, arg)
38 def read_msbuild_xml(path, values=None):
39 """Reads the MS Build XML file at the path and returns its contents.
42 values -- The map to append the contents to (default {})
47 # Attempt to read the file contents
49 document = parse(path)
50 except Exception as e:
51 logging.exception('Could not read MS Build XML file at %s', path)
54 # Convert the XML to JSON format
55 logging.info('Processing MS Build XML file at %s', path)
58 rule = document.getElementsByTagName('Rule')[0]
60 rule_name = rule.attributes['Name'].value
62 logging.info('Found rules for %s', rule_name)
64 # Proprocess Argument values
65 __preprocess_arguments(rule)
69 __convert(rule, 'EnumProperty', converted_values, __convert_enum)
70 __convert(rule, 'BoolProperty', converted_values, __convert_bool)
71 __convert(rule, 'StringListProperty', converted_values,
72 __convert_string_list)
73 __convert(rule, 'StringProperty', converted_values, __convert_string)
74 __convert(rule, 'IntProperty', converted_values, __convert_string)
76 values[rule_name] = converted_values
81 def read_msbuild_json(path, values=None):
82 """Reads the MS Build JSON file at the path and returns its contents.
85 values -- The list to append the contents to (default [])
90 if not os.path.exists(path):
91 logging.info('Could not find MS Build JSON file at %s', path)
95 values.extend(__read_json_file(path))
96 except Exception as e:
97 logging.exception('Could not read MS Build JSON file at %s', path)
100 logging.info('Processing MS Build JSON file at %s', path)
105 """Script entrypoint."""
106 # Parse the arguments
107 parser = argparse.ArgumentParser(
108 description='Convert MSBuild XML to JSON format')
111 '-t', '--toolchain', help='The name of the toolchain', required=True)
113 '-o', '--output', help='The output directory', default='')
117 help='Whether previously output should be overwritten',
120 parser.set_defaults(overwrite=False)
124 help="Debug tool output",
125 action="store_const",
128 default=logging.WARNING)
132 help="Verbose output",
133 action="store_const",
136 parser.add_argument('input', help='The input files', nargs='+')
138 args = parser.parse_args()
140 toolchain = args.toolchain
142 logging.basicConfig(level=args.loglevel)
143 logging.info('Creating %s toolchain files', toolchain)
147 # Iterate through the inputs
148 for input in args.input:
149 input = __get_path(input)
151 read_msbuild_xml(input, values)
153 # Determine if the output directory needs to be created
154 output_dir = __get_path(args.output)
156 if not os.path.exists(output_dir):
158 logging.info('Created output directory %s', output_dir)
160 for key, value in values.items():
161 output_path = __output_path(toolchain, key, output_dir)
163 if os.path.exists(output_path) and not args.overwrite:
164 logging.info('Comparing previous output to current')
166 __merge_json_values(value, read_msbuild_json(output_path))
168 logging.info('Original output will be overwritten')
170 logging.info('Writing MS Build JSON file at %s', output_path)
172 __write_json_file(output_path, value)
175 ###########################################################################################
176 # private joining functions
177 def __merge_json_values(current, previous):
178 """Merges the values between the current and previous run of the script."""
179 for value in current:
182 # Find the previous value
183 previous_value = __find_and_remove_value(previous, value)
185 if previous_value is not None:
186 flags = value['flags']
187 previous_flags = previous_value['flags']
189 if flags != previous_flags:
191 'Flags for %s are different. Using previous value.', name)
193 value['flags'] = previous_flags
195 logging.warning('Value %s is a new value', name)
197 for value in previous:
200 'Value %s not present in current run. Appending value.', name)
202 current.append(value)
205 def __find_and_remove_value(list, compare):
206 """Finds the value in the list that corresponds with the value of compare."""
207 # next throws if there are no matches
209 found = next(value for value in list
210 if value['name'] == compare['name'] and value['switch'] ==
220 def __normalize_switch(switch, separator):
222 if switch.startswith("/") or switch.startswith("-"):
224 if new and separator:
225 new = new + separator
228 ###########################################################################################
229 # private xml functions
230 def __convert(root, tag, values, func):
231 """Converts the tag type found in the root and converts them using the func
232 and appends them to the values.
234 elements = root.getElementsByTagName(tag)
236 for element in elements:
237 converted = func(element)
240 __append_list(values, converted)
243 def __convert_enum(node):
244 """Converts an EnumProperty node to JSON format."""
245 name = __get_attribute(node, 'Name')
246 logging.debug('Found EnumProperty named %s', name)
248 converted_values = []
250 for value in node.getElementsByTagName('EnumValue'):
251 converted = __convert_node(value)
253 converted['value'] = converted['name']
254 converted['name'] = name
256 # Modify flags when there is an argument child
257 __with_argument(value, converted)
259 converted_values.append(converted)
261 return converted_values
264 def __convert_bool(node):
265 """Converts an BoolProperty node to JSON format."""
266 converted = __convert_node(node, default_value='true')
268 # Check for a switch for reversing the value
269 reverse_switch = __get_attribute(node, 'ReverseSwitch')
272 __with_argument(node, converted)
274 converted_reverse = copy.deepcopy(converted)
276 converted_reverse['switch'] = reverse_switch
277 converted_reverse['value'] = 'false'
279 return [converted_reverse, converted]
281 # Modify flags when there is an argument child
282 __with_argument(node, converted)
284 return __check_for_flag(converted)
287 def __convert_string_list(node):
288 """Converts a StringListProperty node to JSON format."""
289 converted = __convert_node(node)
291 # Determine flags for the string list
292 flags = vsflags(VSFlags.UserValue)
294 # Check for a separator to determine if it is semicolon appendable
295 # If not present assume the value should be ;
296 separator = __get_attribute(node, 'Separator', default_value=';')
299 flags = vsflags(flags, VSFlags.SemicolonAppendable)
301 converted['flags'] = flags
303 return __check_for_flag(converted)
306 def __convert_string(node):
307 """Converts a StringProperty node to JSON format."""
308 converted = __convert_node(node, default_flags=vsflags(VSFlags.UserValue))
310 return __check_for_flag(converted)
313 def __convert_node(node, default_value='', default_flags=vsflags()):
314 """Converts a XML node to a JSON equivalent."""
315 name = __get_attribute(node, 'Name')
316 logging.debug('Found %s named %s', node.tagName, name)
319 converted['name'] = name
321 switch = __get_attribute(node, 'Switch')
322 separator = __get_attribute(node, 'Separator')
323 converted['switch'] = __normalize_switch(switch, separator)
325 converted['comment'] = __get_attribute(node, 'DisplayName')
326 converted['value'] = default_value
328 # Check for the Flags attribute in case it was created during preprocessing
329 flags = __get_attribute(node, 'Flags')
332 flags = flags.split(',')
334 flags = default_flags
336 converted['flags'] = flags
341 def __check_for_flag(value):
342 """Checks whether the value has a switch value.
344 If not then returns None as it should not be added.
349 logging.warning('Skipping %s which has no command line switch',
354 def __with_argument(node, value):
355 """Modifies the flags in value if the node contains an Argument."""
356 arguments = node.getElementsByTagName('Argument')
359 logging.debug('Found argument within %s', value['name'])
360 value['flags'] = vsflags(VSFlags.UserValueIgnored, VSFlags.Continue)
363 def __preprocess_arguments(root):
364 """Preprocesses occurrences of Argument within the root.
366 Argument XML values reference other values within the document by name. The
367 referenced value does not contain a switch. This function will add the
368 switch associated with the argument.
370 # Set the flags to require a value
371 flags = ','.join(vsflags(VSFlags.UserValueRequired))
373 # Search through the arguments
374 arguments = root.getElementsByTagName('Argument')
376 for argument in arguments:
377 reference = __get_attribute(argument, 'Property')
380 # Look for the argument within the root's children
381 for child in root.childNodes:
383 if isinstance(child, Element):
384 name = __get_attribute(child, 'Name')
386 if name == reference:
390 if found is not None:
391 logging.info('Found property named %s', reference)
392 # Get the associated switch
393 switch = __get_attribute(argument.parentNode, 'Switch')
395 # See if there is already a switch associated with the element.
396 if __get_attribute(found, 'Switch'):
397 logging.debug('Copying node %s', reference)
398 clone = found.cloneNode(True)
399 root.insertBefore(clone, found)
402 found.setAttribute('Switch', switch)
403 found.setAttribute('Flags', flags)
405 logging.warning('Could not find property named %s', reference)
408 def __get_attribute(node, name, default_value=''):
409 """Retrieves the attribute of the given name from the node.
411 If not present then the default_value is used.
413 if node.hasAttribute(name):
414 return node.attributes[name].value.strip()
419 ###########################################################################################
420 # private path functions
421 def __get_path(path):
422 """Gets the path to the file."""
423 if not os.path.isabs(path):
424 path = os.path.join(os.getcwd(), path)
426 return os.path.normpath(path)
429 def __output_path(toolchain, rule, output_dir):
430 """Gets the output path for a file given the toolchain, rule and output_dir"""
431 filename = '%s_%s.json' % (toolchain, rule)
432 return os.path.join(output_dir, filename)
435 ###########################################################################################
436 # private JSON file functions
437 def __read_json_file(path):
438 """Reads a JSON file at the path."""
439 with open(path, 'r') as f:
443 def __write_json_file(path, values):
444 """Writes a JSON file at the path with the values provided."""
445 # Sort the keys to ensure ordering
446 sort_order = ['name', 'switch', 'comment', 'value', 'flags']
450 value.items(), key=lambda value: sort_order.index(value[0])))
454 with open(path, 'w') as f:
455 json.dump(sorted_values, f, indent=2, separators=(',', ': '))
458 ###########################################################################################
459 # private list helpers
460 def __append_list(append_to, value):
461 """Appends the value to the list."""
462 if value is not None:
463 if isinstance(value, list):
464 append_to.extend(value)
466 append_to.append(value)
468 ###########################################################################################
470 if __name__ == "__main__":