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