1# Distributed under the OSI-approved BSD 3-Clause License. See accompanying 2# file Copyright.txt or https://cmake.org/licensing for details. 3 4import argparse 5import codecs 6import copy 7import logging 8import json 9import os 10 11from collections import OrderedDict 12from xml.dom.minidom import parse, parseString, Element 13 14 15class VSFlags: 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] 26 27 28def vsflags(*args): 29 """Combines the flags.""" 30 values = [] 31 32 for arg in args: 33 __append_list(values, arg) 34 35 return values 36 37 38def read_msbuild_xml(path, values=None): 39 """Reads the MS Build XML file at the path and returns its contents. 40 41 Keyword arguments: 42 values -- The map to append the contents to (default {}) 43 """ 44 if values is None: 45 values = {} 46 47 # Attempt to read the file contents 48 try: 49 document = parse(path) 50 except Exception as e: 51 logging.exception('Could not read MS Build XML file at %s', path) 52 return values 53 54 # Convert the XML to JSON format 55 logging.info('Processing MS Build XML file at %s', path) 56 57 # Get the rule node 58 rule = document.getElementsByTagName('Rule')[0] 59 60 rule_name = rule.attributes['Name'].value 61 62 logging.info('Found rules for %s', rule_name) 63 64 # Proprocess Argument values 65 __preprocess_arguments(rule) 66 67 # Get all the values 68 converted_values = [] 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) 75 76 values[rule_name] = converted_values 77 78 return values 79 80 81def read_msbuild_json(path, values=None): 82 """Reads the MS Build JSON file at the path and returns its contents. 83 84 Keyword arguments: 85 values -- The list to append the contents to (default []) 86 """ 87 if values is None: 88 values = [] 89 90 if not os.path.exists(path): 91 logging.info('Could not find MS Build JSON file at %s', path) 92 return values 93 94 try: 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) 98 return values 99 100 logging.info('Processing MS Build JSON file at %s', path) 101 102 return values 103 104def main(): 105 """Script entrypoint.""" 106 # Parse the arguments 107 parser = argparse.ArgumentParser( 108 description='Convert MSBuild XML to JSON format') 109 110 parser.add_argument( 111 '-t', '--toolchain', help='The name of the toolchain', required=True) 112 parser.add_argument( 113 '-o', '--output', help='The output directory', default='') 114 parser.add_argument( 115 '-r', 116 '--overwrite', 117 help='Whether previously output should be overwritten', 118 dest='overwrite', 119 action='store_true') 120 parser.set_defaults(overwrite=False) 121 parser.add_argument( 122 '-d', 123 '--debug', 124 help="Debug tool output", 125 action="store_const", 126 dest="loglevel", 127 const=logging.DEBUG, 128 default=logging.WARNING) 129 parser.add_argument( 130 '-v', 131 '--verbose', 132 help="Verbose output", 133 action="store_const", 134 dest="loglevel", 135 const=logging.INFO) 136 parser.add_argument('input', help='The input files', nargs='+') 137 138 args = parser.parse_args() 139 140 toolchain = args.toolchain 141 142 logging.basicConfig(level=args.loglevel) 143 logging.info('Creating %s toolchain files', toolchain) 144 145 values = {} 146 147 # Iterate through the inputs 148 for input in args.input: 149 input = __get_path(input) 150 151 read_msbuild_xml(input, values) 152 153 # Determine if the output directory needs to be created 154 output_dir = __get_path(args.output) 155 156 if not os.path.exists(output_dir): 157 os.mkdir(output_dir) 158 logging.info('Created output directory %s', output_dir) 159 160 for key, value in values.items(): 161 output_path = __output_path(toolchain, key, output_dir) 162 163 if os.path.exists(output_path) and not args.overwrite: 164 logging.info('Comparing previous output to current') 165 166 __merge_json_values(value, read_msbuild_json(output_path)) 167 else: 168 logging.info('Original output will be overwritten') 169 170 logging.info('Writing MS Build JSON file at %s', output_path) 171 172 __write_json_file(output_path, value) 173 174 175########################################################################################### 176# private joining functions 177def __merge_json_values(current, previous): 178 """Merges the values between the current and previous run of the script.""" 179 for value in current: 180 name = value['name'] 181 182 # Find the previous value 183 previous_value = __find_and_remove_value(previous, value) 184 185 if previous_value is not None: 186 flags = value['flags'] 187 previous_flags = previous_value['flags'] 188 189 if flags != previous_flags: 190 logging.warning( 191 'Flags for %s are different. Using previous value.', name) 192 193 value['flags'] = previous_flags 194 else: 195 logging.warning('Value %s is a new value', name) 196 197 for value in previous: 198 name = value['name'] 199 logging.warning( 200 'Value %s not present in current run. Appending value.', name) 201 202 current.append(value) 203 204 205def __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 208 try: 209 found = next(value for value in list 210 if value['name'] == compare['name'] and value['switch'] == 211 compare['switch']) 212 except: 213 return None 214 215 list.remove(found) 216 217 return found 218 219 220def __normalize_switch(switch, separator): 221 new = switch 222 if switch.startswith("/") or switch.startswith("-"): 223 new = switch[1:] 224 if new and separator: 225 new = new + separator 226 return new 227 228########################################################################################### 229# private xml functions 230def __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. 233 """ 234 elements = root.getElementsByTagName(tag) 235 236 for element in elements: 237 converted = func(element) 238 239 # Append to the list 240 __append_list(values, converted) 241 242 243def __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) 247 248 converted_values = [] 249 250 for value in node.getElementsByTagName('EnumValue'): 251 converted = __convert_node(value) 252 253 converted['value'] = converted['name'] 254 converted['name'] = name 255 256 # Modify flags when there is an argument child 257 __with_argument(value, converted) 258 259 converted_values.append(converted) 260 261 return converted_values 262 263 264def __convert_bool(node): 265 """Converts an BoolProperty node to JSON format.""" 266 converted = __convert_node(node, default_value='true') 267 268 # Check for a switch for reversing the value 269 reverse_switch = __get_attribute(node, 'ReverseSwitch') 270 271 if reverse_switch: 272 __with_argument(node, converted) 273 274 converted_reverse = copy.deepcopy(converted) 275 276 converted_reverse['switch'] = reverse_switch 277 converted_reverse['value'] = 'false' 278 279 return [converted_reverse, converted] 280 281 # Modify flags when there is an argument child 282 __with_argument(node, converted) 283 284 return __check_for_flag(converted) 285 286 287def __convert_string_list(node): 288 """Converts a StringListProperty node to JSON format.""" 289 converted = __convert_node(node) 290 291 # Determine flags for the string list 292 flags = vsflags(VSFlags.UserValue) 293 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=';') 297 298 if separator == ';': 299 flags = vsflags(flags, VSFlags.SemicolonAppendable) 300 301 converted['flags'] = flags 302 303 return __check_for_flag(converted) 304 305 306def __convert_string(node): 307 """Converts a StringProperty node to JSON format.""" 308 converted = __convert_node(node, default_flags=vsflags(VSFlags.UserValue)) 309 310 return __check_for_flag(converted) 311 312 313def __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) 317 318 converted = {} 319 converted['name'] = name 320 321 switch = __get_attribute(node, 'Switch') 322 separator = __get_attribute(node, 'Separator') 323 converted['switch'] = __normalize_switch(switch, separator) 324 325 converted['comment'] = __get_attribute(node, 'DisplayName') 326 converted['value'] = default_value 327 328 # Check for the Flags attribute in case it was created during preprocessing 329 flags = __get_attribute(node, 'Flags') 330 331 if flags: 332 flags = flags.split(',') 333 else: 334 flags = default_flags 335 336 converted['flags'] = flags 337 338 return converted 339 340 341def __check_for_flag(value): 342 """Checks whether the value has a switch value. 343 344 If not then returns None as it should not be added. 345 """ 346 if value['switch']: 347 return value 348 else: 349 logging.warning('Skipping %s which has no command line switch', 350 value['name']) 351 return None 352 353 354def __with_argument(node, value): 355 """Modifies the flags in value if the node contains an Argument.""" 356 arguments = node.getElementsByTagName('Argument') 357 358 if arguments: 359 logging.debug('Found argument within %s', value['name']) 360 value['flags'] = vsflags(VSFlags.UserValueIgnored, VSFlags.Continue) 361 362 363def __preprocess_arguments(root): 364 """Preprocesses occurrences of Argument within the root. 365 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. 369 """ 370 # Set the flags to require a value 371 flags = ','.join(vsflags(VSFlags.UserValueRequired)) 372 373 # Search through the arguments 374 arguments = root.getElementsByTagName('Argument') 375 376 for argument in arguments: 377 reference = __get_attribute(argument, 'Property') 378 found = None 379 380 # Look for the argument within the root's children 381 for child in root.childNodes: 382 # Ignore Text nodes 383 if isinstance(child, Element): 384 name = __get_attribute(child, 'Name') 385 386 if name == reference: 387 found = child 388 break 389 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') 394 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) 400 found = clone 401 402 found.setAttribute('Switch', switch) 403 found.setAttribute('Flags', flags) 404 else: 405 logging.warning('Could not find property named %s', reference) 406 407 408def __get_attribute(node, name, default_value=''): 409 """Retrieves the attribute of the given name from the node. 410 411 If not present then the default_value is used. 412 """ 413 if node.hasAttribute(name): 414 return node.attributes[name].value.strip() 415 else: 416 return default_value 417 418 419########################################################################################### 420# private path functions 421def __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) 425 426 return os.path.normpath(path) 427 428 429def __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) 433 434 435########################################################################################### 436# private JSON file functions 437def __read_json_file(path): 438 """Reads a JSON file at the path.""" 439 with open(path, 'r') as f: 440 return json.load(f) 441 442 443def __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'] 447 sorted_values = [ 448 OrderedDict( 449 sorted( 450 value.items(), key=lambda value: sort_order.index(value[0]))) 451 for value in values 452 ] 453 454 with open(path, 'w') as f: 455 json.dump(sorted_values, f, indent=2, separators=(',', ': ')) 456 f.write("\n") 457 458########################################################################################### 459# private list helpers 460def __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) 465 else: 466 append_to.append(value) 467 468########################################################################################### 469# main entry point 470if __name__ == "__main__": 471 main() 472