1#!/usr/bin/env python 2 3# Copyright (c) 2012 Google Inc. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Make the format of a vcproj really pretty. 8 9 This script normalize and sort an xml. It also fetches all the properties 10 inside linked vsprops and include them explicitly in the vcproj. 11 12 It outputs the resulting xml to stdout. 13""" 14 15from __future__ import print_function 16 17__author__ = 'nsylvain (Nicolas Sylvain)' 18 19import os 20import sys 21 22from xml.dom.minidom import parse 23from xml.dom.minidom import Node 24 25try: 26 # cmp was removed in python3. 27 cmp 28except NameError: 29 def cmp(a, b): 30 return (a > b) - (a < b) 31 32REPLACEMENTS = dict() 33ARGUMENTS = None 34 35 36class CmpTuple(object): 37 """Compare function between 2 tuple.""" 38 def __call__(self, x, y): 39 return cmp(x[0], y[0]) 40 41 42class CmpNode(object): 43 """Compare function between 2 xml nodes.""" 44 45 def __call__(self, x, y): 46 def get_string(node): 47 node_string = "node" 48 node_string += node.nodeName 49 if node.nodeValue: 50 node_string += node.nodeValue 51 52 if node.attributes: 53 # We first sort by name, if present. 54 node_string += node.getAttribute("Name") 55 56 all_nodes = [] 57 for (name, value) in node.attributes.items(): 58 all_nodes.append((name, value)) 59 60 all_nodes.sort(CmpTuple()) 61 for (name, value) in all_nodes: 62 node_string += name 63 node_string += value 64 65 return node_string 66 67 return cmp(get_string(x), get_string(y)) 68 69 70def PrettyPrintNode(node, indent=0): 71 if node.nodeType == Node.TEXT_NODE: 72 if node.data.strip(): 73 print('%s%s' % (' '*indent, node.data.strip())) 74 return 75 76 if node.childNodes: 77 node.normalize() 78 # Get the number of attributes 79 attr_count = 0 80 if node.attributes: 81 attr_count = node.attributes.length 82 83 # Print the main tag 84 if attr_count == 0: 85 print('%s<%s>' % (' '*indent, node.nodeName)) 86 else: 87 print('%s<%s' % (' '*indent, node.nodeName)) 88 89 all_attributes = [] 90 for (name, value) in node.attributes.items(): 91 all_attributes.append((name, value)) 92 all_attributes.sort(key=(lambda attr: attr[0])) 93 for (name, value) in all_attributes: 94 print('%s %s="%s"' % (' '*indent, name, value)) 95 print('%s>' % (' '*indent)) 96 if node.nodeValue: 97 print('%s %s' % (' '*indent, node.nodeValue)) 98 99 for sub_node in node.childNodes: 100 PrettyPrintNode(sub_node, indent=indent+2) 101 print('%s</%s>' % (' '*indent, node.nodeName)) 102 103 104def FlattenFilter(node): 105 """Returns a list of all the node and sub nodes.""" 106 node_list = [] 107 108 if (node.attributes and 109 node.getAttribute('Name') == '_excluded_files'): 110 # We don't add the "_excluded_files" filter. 111 return [] 112 113 for current in node.childNodes: 114 if current.nodeName == 'Filter': 115 node_list.extend(FlattenFilter(current)) 116 else: 117 node_list.append(current) 118 119 return node_list 120 121 122def FixFilenames(filenames, current_directory): 123 new_list = [] 124 for filename in filenames: 125 if filename: 126 for key in REPLACEMENTS: 127 filename = filename.replace(key, REPLACEMENTS[key]) 128 os.chdir(current_directory) 129 filename = filename.strip('"\' ') 130 if filename.startswith('$'): 131 new_list.append(filename) 132 else: 133 new_list.append(os.path.abspath(filename)) 134 return new_list 135 136 137def AbsoluteNode(node): 138 """Makes all the properties we know about in this node absolute.""" 139 if node.attributes: 140 for (name, value) in node.attributes.items(): 141 if name in ['InheritedPropertySheets', 'RelativePath', 142 'AdditionalIncludeDirectories', 143 'IntermediateDirectory', 'OutputDirectory', 144 'AdditionalLibraryDirectories']: 145 # We want to fix up these paths 146 path_list = value.split(';') 147 new_list = FixFilenames(path_list, os.path.dirname(ARGUMENTS[1])) 148 node.setAttribute(name, ';'.join(new_list)) 149 if not value: 150 node.removeAttribute(name) 151 152 153def CleanupVcproj(node): 154 """For each sub node, we call recursively this function.""" 155 for sub_node in node.childNodes: 156 AbsoluteNode(sub_node) 157 CleanupVcproj(sub_node) 158 159 # Normalize the node, and remove all extranous whitespaces. 160 for sub_node in node.childNodes: 161 if sub_node.nodeType == Node.TEXT_NODE: 162 sub_node.data = sub_node.data.replace("\r", "") 163 sub_node.data = sub_node.data.replace("\n", "") 164 sub_node.data = sub_node.data.rstrip() 165 166 # Fix all the semicolon separated attributes to be sorted, and we also 167 # remove the dups. 168 if node.attributes: 169 for (name, value) in node.attributes.items(): 170 sorted_list = sorted(value.split(';')) 171 unique_list = [] 172 for i in sorted_list: 173 if not unique_list.count(i): 174 unique_list.append(i) 175 node.setAttribute(name, ';'.join(unique_list)) 176 if not value: 177 node.removeAttribute(name) 178 179 if node.childNodes: 180 node.normalize() 181 182 # For each node, take a copy, and remove it from the list. 183 node_array = [] 184 while node.childNodes and node.childNodes[0]: 185 # Take a copy of the node and remove it from the list. 186 current = node.childNodes[0] 187 node.removeChild(current) 188 189 # If the child is a filter, we want to append all its children 190 # to this same list. 191 if current.nodeName == 'Filter': 192 node_array.extend(FlattenFilter(current)) 193 else: 194 node_array.append(current) 195 196 197 # Sort the list. 198 node_array.sort(CmpNode()) 199 200 # Insert the nodes in the correct order. 201 for new_node in node_array: 202 # But don't append empty tool node. 203 if new_node.nodeName == 'Tool': 204 if new_node.attributes and new_node.attributes.length == 1: 205 # This one was empty. 206 continue 207 if new_node.nodeName == 'UserMacro': 208 continue 209 node.appendChild(new_node) 210 211 212def GetConfiguationNodes(vcproj): 213 #TODO(nsylvain): Find a better way to navigate the xml. 214 nodes = [] 215 for node in vcproj.childNodes: 216 if node.nodeName == "Configurations": 217 for sub_node in node.childNodes: 218 if sub_node.nodeName == "Configuration": 219 nodes.append(sub_node) 220 221 return nodes 222 223 224def GetChildrenVsprops(filename): 225 dom = parse(filename) 226 if dom.documentElement.attributes: 227 vsprops = dom.documentElement.getAttribute('InheritedPropertySheets') 228 return FixFilenames(vsprops.split(';'), os.path.dirname(filename)) 229 return [] 230 231def SeekToNode(node1, child2): 232 # A text node does not have properties. 233 if child2.nodeType == Node.TEXT_NODE: 234 return None 235 236 # Get the name of the current node. 237 current_name = child2.getAttribute("Name") 238 if not current_name: 239 # There is no name. We don't know how to merge. 240 return None 241 242 # Look through all the nodes to find a match. 243 for sub_node in node1.childNodes: 244 if sub_node.nodeName == child2.nodeName: 245 name = sub_node.getAttribute("Name") 246 if name == current_name: 247 return sub_node 248 249 # No match. We give up. 250 return None 251 252 253def MergeAttributes(node1, node2): 254 # No attributes to merge? 255 if not node2.attributes: 256 return 257 258 for (name, value2) in node2.attributes.items(): 259 # Don't merge the 'Name' attribute. 260 if name == 'Name': 261 continue 262 value1 = node1.getAttribute(name) 263 if value1: 264 # The attribute exist in the main node. If it's equal, we leave it 265 # untouched, otherwise we concatenate it. 266 if value1 != value2: 267 node1.setAttribute(name, ';'.join([value1, value2])) 268 else: 269 # The attribute does nto exist in the main node. We append this one. 270 node1.setAttribute(name, value2) 271 272 # If the attribute was a property sheet attributes, we remove it, since 273 # they are useless. 274 if name == 'InheritedPropertySheets': 275 node1.removeAttribute(name) 276 277 278def MergeProperties(node1, node2): 279 MergeAttributes(node1, node2) 280 for child2 in node2.childNodes: 281 child1 = SeekToNode(node1, child2) 282 if child1: 283 MergeProperties(child1, child2) 284 else: 285 node1.appendChild(child2.cloneNode(True)) 286 287 288def main(argv): 289 """Main function of this vcproj prettifier.""" 290 global ARGUMENTS 291 ARGUMENTS = argv 292 293 # check if we have exactly 1 parameter. 294 if len(argv) < 2: 295 print('Usage: %s "c:\\path\\to\\vcproj.vcproj" [key1=value1] ' 296 '[key2=value2]' % argv[0]) 297 return 1 298 299 # Parse the keys 300 for i in range(2, len(argv)): 301 (key, value) = argv[i].split('=') 302 REPLACEMENTS[key] = value 303 304 # Open the vcproj and parse the xml. 305 dom = parse(argv[1]) 306 307 # First thing we need to do is find the Configuration Node and merge them 308 # with the vsprops they include. 309 for configuration_node in GetConfiguationNodes(dom.documentElement): 310 # Get the property sheets associated with this configuration. 311 vsprops = configuration_node.getAttribute('InheritedPropertySheets') 312 313 # Fix the filenames to be absolute. 314 vsprops_list = FixFilenames(vsprops.strip().split(';'), 315 os.path.dirname(argv[1])) 316 317 # Extend the list of vsprops with all vsprops contained in the current 318 # vsprops. 319 for current_vsprops in vsprops_list: 320 vsprops_list.extend(GetChildrenVsprops(current_vsprops)) 321 322 # Now that we have all the vsprops, we need to merge them. 323 for current_vsprops in vsprops_list: 324 MergeProperties(configuration_node, 325 parse(current_vsprops).documentElement) 326 327 # Now that everything is merged, we need to cleanup the xml. 328 CleanupVcproj(dom.documentElement) 329 330 # Finally, we use the prett xml function to print the vcproj back to the 331 # user. 332 PrettyPrintNode(dom.documentElement) 333 return 0 334 335 336if __name__ == '__main__': 337 sys.exit(main(sys.argv)) 338