summaryrefslogtreecommitdiff
path: root/Source/cmConvertMSBuildXMLToJSON.py
diff options
context:
space:
mode:
authorDon Olmstead <don.j.olmstead@gmail.com>2016-10-10 17:52:49 -0700
committerBrad King <brad.king@kitware.com>2016-10-12 14:40:18 -0400
commitccdc3d300f3f0f3e8492e580a21513b9664c9aca (patch)
tree97a69bf6e809720960bdbe00499bb050b9bba05d /Source/cmConvertMSBuildXMLToJSON.py
parent06b71ff9fb41369e6dce0c2b9760d2d546d47dec (diff)
downloadcmake-ccdc3d300f3f0f3e8492e580a21513b9664c9aca.tar.gz
Add a script to convert from MSBuild XML to a JSON format
This will supersede the `cmparseMSBuildXML.py` script once we have support for loading the JSON files instead of using hard-coded flag tables.
Diffstat (limited to 'Source/cmConvertMSBuildXMLToJSON.py')
-rw-r--r--Source/cmConvertMSBuildXMLToJSON.py453
1 files changed, 453 insertions, 0 deletions
diff --git a/Source/cmConvertMSBuildXMLToJSON.py b/Source/cmConvertMSBuildXMLToJSON.py
new file mode 100644
index 0000000000..93ab8a82f0
--- /dev/null
+++ b/Source/cmConvertMSBuildXMLToJSON.py
@@ -0,0 +1,453 @@
+# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
+# file Copyright.txt or https://cmake.org/licensing for details.
+
+import argparse
+import codecs
+import copy
+import logging
+import json
+import os
+
+from collections import OrderedDict
+from xml.dom.minidom import parse, parseString, Element
+
+
+class VSFlags:
+ """Flags corresponding to cmIDEFlagTable."""
+ UserValue = "UserValue" # (1 << 0)
+ UserIgnored = "UserIgnored" # (1 << 1)
+ UserRequired = "UserRequired" # (1 << 2)
+ Continue = "Continue" #(1 << 3)
+ SemicolonAppendable = "SemicolonAppendable" # (1 << 4)
+ UserFollowing = "UserFollowing" # (1 << 5)
+ CaseInsensitive = "CaseInsensitive" # (1 << 6)
+ UserValueIgnored = [UserValue, UserIgnored]
+ UserValueRequired = [UserValue, UserRequired]
+
+
+def vsflags(*args):
+ """Combines the flags."""
+ values = []
+
+ for arg in args:
+ __append_list(values, arg)
+
+ return values
+
+
+def read_msbuild_xml(path, values={}):
+ """Reads the MS Build XML file at the path and returns its contents.
+
+ Keyword arguments:
+ values -- The map to append the contents to (default {})
+ """
+
+ # Attempt to read the file contents
+ try:
+ document = parse(path)
+ except Exception as e:
+ logging.exception('Could not read MS Build XML file at %s', path)
+ return values
+
+ # Convert the XML to JSON format
+ logging.info('Processing MS Build XML file at %s', path)
+
+ # Get the rule node
+ rule = document.getElementsByTagName('Rule')[0]
+
+ rule_name = rule.attributes['Name'].value
+
+ logging.info('Found rules for %s', rule_name)
+
+ # Proprocess Argument values
+ __preprocess_arguments(rule)
+
+ # Get all the values
+ converted_values = []
+ __convert(rule, 'EnumProperty', converted_values, __convert_enum)
+ __convert(rule, 'BoolProperty', converted_values, __convert_bool)
+ __convert(rule, 'StringListProperty', converted_values,
+ __convert_string_list)
+ __convert(rule, 'StringProperty', converted_values, __convert_string)
+ __convert(rule, 'IntProperty', converted_values, __convert_string)
+
+ values[rule_name] = converted_values
+
+ return values
+
+
+def read_msbuild_json(path, values=[]):
+ """Reads the MS Build JSON file at the path and returns its contents.
+
+ Keyword arguments:
+ values -- The list to append the contents to (default [])
+ """
+ if not os.path.exists(path):
+ logging.info('Could not find MS Build JSON file at %s', path)
+ return values
+
+ try:
+ values.extend(__read_json_file(path))
+ except Exception as e:
+ logging.exception('Could not read MS Build JSON file at %s', path)
+ return values
+
+ logging.info('Processing MS Build JSON file at %s', path)
+
+ return values
+
+
+def main():
+ """Script entrypoint."""
+ # Parse the arguments
+ parser = argparse.ArgumentParser(
+ description='Convert MSBuild XML to JSON format')
+
+ parser.add_argument(
+ '-t', '--toolchain', help='The name of the toolchain', required=True)
+ parser.add_argument(
+ '-o', '--output', help='The output directory', default='')
+ parser.add_argument(
+ '-r',
+ '--overwrite',
+ help='Whether previously output should be overwritten',
+ dest='overwrite',
+ action='store_true')
+ parser.set_defaults(overwrite=False)
+ parser.add_argument(
+ '-d',
+ '--debug',
+ help="Debug tool output",
+ action="store_const",
+ dest="loglevel",
+ const=logging.DEBUG,
+ default=logging.WARNING)
+ parser.add_argument(
+ '-v',
+ '--verbose',
+ help="Verbose output",
+ action="store_const",
+ dest="loglevel",
+ const=logging.INFO)
+ parser.add_argument('input', help='The input files', nargs='+')
+
+ args = parser.parse_args()
+
+ toolchain = args.toolchain
+
+ logging.basicConfig(level=args.loglevel)
+ logging.info('Creating %s toolchain files', toolchain)
+
+ values = {}
+
+ # Iterate through the inputs
+ for input in args.input:
+ input = __get_path(input)
+
+ read_msbuild_xml(input, values)
+
+ # Determine if the output directory needs to be created
+ output_dir = __get_path(args.output)
+
+ if not os.path.exists(output_dir):
+ os.mkdir(output_dir)
+ logging.info('Created output directory %s', output_dir)
+
+ for key, value in values.items():
+ output_path = __output_path(toolchain, key, output_dir)
+
+ if os.path.exists(output_path) and not args.overwrite:
+ logging.info('Comparing previous output to current')
+
+ __merge_json_values(value, read_msbuild_json(output_path))
+ else:
+ logging.info('Original output will be overwritten')
+
+ logging.info('Writing MS Build JSON file at %s', output_path)
+
+ __write_json_file(output_path, value)
+
+
+###########################################################################################
+# private joining functions
+def __merge_json_values(current, previous):
+ """Merges the values between the current and previous run of the script."""
+ for value in current:
+ name = value['name']
+
+ # Find the previous value
+ previous_value = __find_and_remove_value(previous, value)
+
+ if previous_value is not None:
+ flags = value['flags']
+ previous_flags = previous_value['flags']
+
+ if flags != previous_flags:
+ logging.warning(
+ 'Flags for %s are different. Using previous value.', name)
+
+ value['flags'] = previous_flags
+ else:
+ logging.warning('Value %s is a new value', name)
+
+ for value in previous:
+ name = value['name']
+ logging.warning(
+ 'Value %s not present in current run. Appending value.', name)
+
+ current.append(value)
+
+
+def __find_and_remove_value(list, compare):
+ """Finds the value in the list that corresponds with the value of compare."""
+ # next throws if there are no matches
+ try:
+ found = next(value for value in list
+ if value['name'] == compare['name'] and value['switch'] ==
+ compare['switch'])
+ except:
+ return None
+
+ list.remove(found)
+
+ return found
+
+
+###########################################################################################
+# private xml functions
+def __convert(root, tag, values, func):
+ """Converts the tag type found in the root and converts them using the func
+ and appends them to the values.
+ """
+ elements = root.getElementsByTagName(tag)
+
+ for element in elements:
+ converted = func(element)
+
+ # Append to the list
+ __append_list(values, converted)
+
+
+def __convert_enum(node):
+ """Converts an EnumProperty node to JSON format."""
+ name = __get_attribute(node, 'Name')
+ logging.debug('Found EnumProperty named %s', name)
+
+ converted_values = []
+
+ for value in node.getElementsByTagName('EnumValue'):
+ converted = __convert_node(value)
+
+ converted['value'] = converted['name']
+ converted['name'] = name
+
+ # Modify flags when there is an argument child
+ __with_argument(value, converted)
+
+ converted_values.append(converted)
+
+ return converted_values
+
+
+def __convert_bool(node):
+ """Converts an BoolProperty node to JSON format."""
+ converted = __convert_node(node, default_value='true')
+
+ # Check for a switch for reversing the value
+ reverse_switch = __get_attribute(node, 'ReverseSwitch')
+
+ if reverse_switch:
+ converted_reverse = copy.deepcopy(converted)
+
+ converted_reverse['switch'] = reverse_switch
+ converted_reverse['value'] = 'false'
+
+ return [converted_reverse, converted]
+
+ # Modify flags when there is an argument child
+ __with_argument(node, converted)
+
+ return __check_for_flag(converted)
+
+
+def __convert_string_list(node):
+ """Converts a StringListProperty node to JSON format."""
+ converted = __convert_node(node)
+
+ # Determine flags for the string list
+ flags = vsflags(VSFlags.UserValue)
+
+ # Check for a separator to determine if it is semicolon appendable
+ # If not present assume the value should be ;
+ separator = __get_attribute(node, 'Separator', default_value=';')
+
+ if separator == ';':
+ flags = vsflags(flags, VSFlags.SemicolonAppendable)
+
+ converted['flags'] = flags
+
+ return __check_for_flag(converted)
+
+
+def __convert_string(node):
+ """Converts a StringProperty node to JSON format."""
+ converted = __convert_node(node, default_flags=vsflags(VSFlags.UserValue))
+
+ return __check_for_flag(converted)
+
+
+def __convert_node(node, default_value='', default_flags=vsflags()):
+ """Converts a XML node to a JSON equivalent."""
+ name = __get_attribute(node, 'Name')
+ logging.debug('Found %s named %s', node.tagName, name)
+
+ converted = {}
+ converted['name'] = name
+ converted['switch'] = __get_attribute(node, 'Switch')
+ converted['comment'] = __get_attribute(node, 'DisplayName')
+ converted['value'] = default_value
+
+ # Check for the Flags attribute in case it was created during preprocessing
+ flags = __get_attribute(node, 'Flags')
+
+ if flags:
+ flags = flags.split(',')
+ else:
+ flags = default_flags
+
+ converted['flags'] = flags
+
+ return converted
+
+
+def __check_for_flag(value):
+ """Checks whether the value has a switch value.
+
+ If not then returns None as it should not be added.
+ """
+ if value['switch']:
+ return value
+ else:
+ logging.warning('Skipping %s which has no command line switch',
+ value['name'])
+ return None
+
+
+def __with_argument(node, value):
+ """Modifies the flags in value if the node contains an Argument."""
+ arguments = node.getElementsByTagName('Argument')
+
+ if arguments:
+ logging.debug('Found argument within %s', value['name'])
+ value['flags'] = vsflags(VSFlags.UserValueIgnored, VSFlags.Continue)
+
+
+def __preprocess_arguments(root):
+ """Preprocesses occurrances of Argument within the root.
+
+ Argument XML values reference other values within the document by name. The
+ referenced value does not contain a switch. This function will add the
+ switch associated with the argument.
+ """
+ # Set the flags to require a value
+ flags = ','.join(vsflags(VSFlags.UserValueRequired))
+
+ # Search through the arguments
+ arguments = root.getElementsByTagName('Argument')
+
+ for argument in arguments:
+ reference = __get_attribute(argument, 'Property')
+ found = None
+
+ # Look for the argument within the root's children
+ for child in root.childNodes:
+ # Ignore Text nodes
+ if isinstance(child, Element):
+ name = __get_attribute(child, 'Name')
+
+ if name == reference:
+ found = child
+ break
+
+ if found is not None:
+ logging.info('Found property named %s', reference)
+ # Get the associated switch
+ switch = __get_attribute(argument.parentNode, 'Switch')
+
+ # See if there is already a switch associated with the element.
+ if __get_attribute(found, 'Switch'):
+ logging.debug('Copying node %s', reference)
+ clone = found.cloneNode(True)
+ root.insertBefore(clone, found)
+ found = clone
+
+ found.setAttribute('Switch', switch)
+ found.setAttribute('Flags', flags)
+ else:
+ logging.warning('Could not find property named %s', reference)
+
+
+def __get_attribute(node, name, default_value=''):
+ """Retrieves the attribute of the given name from the node.
+
+ If not present then the default_value is used.
+ """
+ if node.hasAttribute(name):
+ return node.attributes[name].value.strip()
+ else:
+ return default_value
+
+
+###########################################################################################
+# private path functions
+def __get_path(path):
+ """Gets the path to the file."""
+ if not os.path.isabs(path):
+ path = os.path.join(os.getcwd(), path)
+
+ return os.path.normpath(path)
+
+
+def __output_path(toolchain, rule, output_dir):
+ """Gets the output path for a file given the toolchain, rule and output_dir"""
+ filename = '%s_%s.json' % (toolchain, rule)
+ return os.path.join(output_dir, filename)
+
+
+###########################################################################################
+# private JSON file functions
+def __read_json_file(path):
+ """Reads a JSON file at the path."""
+ with open(path, 'r') as f:
+ return json.load(f)
+
+
+def __write_json_file(path, values):
+ """Writes a JSON file at the path with the values provided."""
+ # Sort the keys to ensure ordering
+ sort_order = ['name', 'switch', 'comment', 'value', 'flags']
+ sorted_values = [
+ OrderedDict(
+ sorted(
+ value.items(), key=lambda value: sort_order.index(value[0])))
+ for value in values
+ ]
+
+ with open(path, 'w') as f:
+ json.dump(sorted_values, f, indent=2, separators=(',', ': '))
+
+
+###########################################################################################
+# private list helpers
+def __append_list(append_to, value):
+ """Appends the value to the list."""
+ if value is not None:
+ if isinstance(value, list):
+ append_to.extend(value)
+ else:
+ append_to.append(value)
+
+###########################################################################################
+# main entry point
+if __name__ == "__main__":
+ main()