# 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()