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