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