| # 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=None): | |
| """Reads the MS Build XML file at the path and returns its contents. | |
| Keyword arguments: | |
| values -- The map to append the contents to (default {}) | |
| """ | |
| if values is None: | |
| values = {} | |
| # 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=None): | |
| """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 values is None: | |
| values = [] | |
| 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 | |
| def __normalize_switch(switch, separator): | |
| new = switch | |
| if switch.startswith("/") or switch.startswith("-"): | |
| new = switch[1:] | |
| if new and separator: | |
| new = new + separator | |
| return new | |
| ########################################################################################### | |
| # 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: | |
| __with_argument(node, converted) | |
| 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 | |
| switch = __get_attribute(node, 'Switch') | |
| separator = __get_attribute(node, 'Separator') | |
| converted['switch'] = __normalize_switch(switch, separator) | |
| 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 occurrences 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=(',', ': ')) | |
| f.write("\n") | |
| ########################################################################################### | |
| # 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() |