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
17import os
18import sys
19
20from xml.dom.minidom import parse
21from xml.dom.minidom import Node
22
23REPLACEMENTS = dict()
24ARGUMENTS = None
25
26
27class CmpTuple(object):
28  """Compare function between 2 tuple."""
29  def __call__(self, x, y):
30    return cmp(x[0], y[0])
31
32
33class 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
61def 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
95def 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
113def 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
128def 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
144def 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
203def 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
215def 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
222def 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
244def 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
269def 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
279def 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
328if __name__ == '__main__':
329  sys.exit(main(sys.argv))
330