| 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 |
|
|---|
| 16 | import os
|
|---|
| 17 | import sys
|
|---|
| 18 |
|
|---|
| 19 | from xml.dom.minidom import parse
|
|---|
| 20 | from xml.dom.minidom import Node
|
|---|
| 21 |
|
|---|
| 22 | __author__ = "nsylvain (Nicolas Sylvain)"
|
|---|
| 23 | ARGUMENTS = None
|
|---|
| 24 | REPLACEMENTS = dict()
|
|---|
| 25 |
|
|---|
| 26 |
|
|---|
| 27 | def cmp(x, y):
|
|---|
| 28 | return (x > y) - (x < y)
|
|---|
| 29 |
|
|---|
| 30 |
|
|---|
| 31 | class CmpTuple:
|
|---|
| 32 | """Compare function between 2 tuple."""
|
|---|
| 33 |
|
|---|
| 34 | def __call__(self, x, y):
|
|---|
| 35 | return cmp(x[0], y[0])
|
|---|
| 36 |
|
|---|
| 37 |
|
|---|
| 38 | class 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 |
|
|---|
| 66 | def 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 |
|
|---|
| 100 | def 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 |
|
|---|
| 117 | def 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 |
|
|---|
| 132 | def 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 |
|
|---|
| 152 | def 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 |
|
|---|
| 210 | def 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 |
|
|---|
| 222 | def 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 |
|
|---|
| 230 | def 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 |
|
|---|
| 252 | def 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 |
|
|---|
| 277 | def 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 |
|
|---|
| 287 | def 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 |
|
|---|
| 338 | if __name__ == "__main__":
|
|---|
| 339 | sys.exit(main(sys.argv))
|
|---|