1# Copyright (c) 2012 Google Inc. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""New implementation of Visual Studio project generation."""
6
7import os
8import random
9
10import gyp.common
11
12# hashlib is supplied as of Python 2.5 as the replacement interface for md5
13# and other secure hashes.  In 2.6, md5 is deprecated.  Import hashlib if
14# available, avoiding a deprecation warning under 2.6.  Import md5 otherwise,
15# preserving 2.4 compatibility.
16try:
17  import hashlib
18  _new_md5 = hashlib.md5
19except ImportError:
20  import md5
21  _new_md5 = md5.new
22
23
24# Initialize random number generator
25random.seed()
26
27# GUIDs for project types
28ENTRY_TYPE_GUIDS = {
29    'project': '{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}',
30    'folder': '{2150E333-8FDC-42A3-9474-1A3956D46DE8}',
31}
32
33#------------------------------------------------------------------------------
34# Helper functions
35
36
37def MakeGuid(name, seed='msvs_new'):
38  """Returns a GUID for the specified target name.
39
40  Args:
41    name: Target name.
42    seed: Seed for MD5 hash.
43  Returns:
44    A GUID-line string calculated from the name and seed.
45
46  This generates something which looks like a GUID, but depends only on the
47  name and seed.  This means the same name/seed will always generate the same
48  GUID, so that projects and solutions which refer to each other can explicitly
49  determine the GUID to refer to explicitly.  It also means that the GUID will
50  not change when the project for a target is rebuilt.
51  """
52  # Calculate a MD5 signature for the seed and name.
53  d = _new_md5(str(seed) + str(name)).hexdigest().upper()
54  # Convert most of the signature to GUID form (discard the rest)
55  guid = ('{' + d[:8] + '-' + d[8:12] + '-' + d[12:16] + '-' + d[16:20]
56          + '-' + d[20:32] + '}')
57  return guid
58
59#------------------------------------------------------------------------------
60
61
62class MSVSSolutionEntry(object):
63  def __cmp__(self, other):
64    # Sort by name then guid (so things are in order on vs2008).
65    return cmp((self.name, self.get_guid()), (other.name, other.get_guid()))
66
67
68class MSVSFolder(MSVSSolutionEntry):
69  """Folder in a Visual Studio project or solution."""
70
71  def __init__(self, path, name = None, entries = None,
72               guid = None, items = None):
73    """Initializes the folder.
74
75    Args:
76      path: Full path to the folder.
77      name: Name of the folder.
78      entries: List of folder entries to nest inside this folder.  May contain
79          Folder or Project objects.  May be None, if the folder is empty.
80      guid: GUID to use for folder, if not None.
81      items: List of solution items to include in the folder project.  May be
82          None, if the folder does not directly contain items.
83    """
84    if name:
85      self.name = name
86    else:
87      # Use last layer.
88      self.name = os.path.basename(path)
89
90    self.path = path
91    self.guid = guid
92
93    # Copy passed lists (or set to empty lists)
94    self.entries = sorted(list(entries or []))
95    self.items = list(items or [])
96
97    self.entry_type_guid = ENTRY_TYPE_GUIDS['folder']
98
99  def get_guid(self):
100    if self.guid is None:
101      # Use consistent guids for folders (so things don't regenerate).
102      self.guid = MakeGuid(self.path, seed='msvs_folder')
103    return self.guid
104
105
106#------------------------------------------------------------------------------
107
108
109class MSVSProject(MSVSSolutionEntry):
110  """Visual Studio project."""
111
112  def __init__(self, path, name = None, dependencies = None, guid = None,
113               spec = None, build_file = None, config_platform_overrides = None,
114               fixpath_prefix = None):
115    """Initializes the project.
116
117    Args:
118      path: Absolute path to the project file.
119      name: Name of project.  If None, the name will be the same as the base
120          name of the project file.
121      dependencies: List of other Project objects this project is dependent
122          upon, if not None.
123      guid: GUID to use for project, if not None.
124      spec: Dictionary specifying how to build this project.
125      build_file: Filename of the .gyp file that the vcproj file comes from.
126      config_platform_overrides: optional dict of configuration platforms to
127          used in place of the default for this target.
128      fixpath_prefix: the path used to adjust the behavior of _fixpath
129    """
130    self.path = path
131    self.guid = guid
132    self.spec = spec
133    self.build_file = build_file
134    # Use project filename if name not specified
135    self.name = name or os.path.splitext(os.path.basename(path))[0]
136
137    # Copy passed lists (or set to empty lists)
138    self.dependencies = list(dependencies or [])
139
140    self.entry_type_guid = ENTRY_TYPE_GUIDS['project']
141
142    if config_platform_overrides:
143      self.config_platform_overrides = config_platform_overrides
144    else:
145      self.config_platform_overrides = {}
146    self.fixpath_prefix = fixpath_prefix
147    self.msbuild_toolset = None
148
149  def set_dependencies(self, dependencies):
150    self.dependencies = list(dependencies or [])
151
152  def get_guid(self):
153    if self.guid is None:
154      # Set GUID from path
155      # TODO(rspangler): This is fragile.
156      # 1. We can't just use the project filename sans path, since there could
157      #    be multiple projects with the same base name (for example,
158      #    foo/unittest.vcproj and bar/unittest.vcproj).
159      # 2. The path needs to be relative to $SOURCE_ROOT, so that the project
160      #    GUID is the same whether it's included from base/base.sln or
161      #    foo/bar/baz/baz.sln.
162      # 3. The GUID needs to be the same each time this builder is invoked, so
163      #    that we don't need to rebuild the solution when the project changes.
164      # 4. We should be able to handle pre-built project files by reading the
165      #    GUID from the files.
166      self.guid = MakeGuid(self.name)
167    return self.guid
168
169  def set_msbuild_toolset(self, msbuild_toolset):
170    self.msbuild_toolset = msbuild_toolset
171
172#------------------------------------------------------------------------------
173
174
175class MSVSSolution(object):
176  """Visual Studio solution."""
177
178  def __init__(self, path, version, entries=None, variants=None,
179               websiteProperties=True):
180    """Initializes the solution.
181
182    Args:
183      path: Path to solution file.
184      version: Format version to emit.
185      entries: List of entries in solution.  May contain Folder or Project
186          objects.  May be None, if the folder is empty.
187      variants: List of build variant strings.  If none, a default list will
188          be used.
189      websiteProperties: Flag to decide if the website properties section
190          is generated.
191    """
192    self.path = path
193    self.websiteProperties = websiteProperties
194    self.version = version
195
196    # Copy passed lists (or set to empty lists)
197    self.entries = list(entries or [])
198
199    if variants:
200      # Copy passed list
201      self.variants = variants[:]
202    else:
203      # Use default
204      self.variants = ['Debug|Win32', 'Release|Win32']
205    # TODO(rspangler): Need to be able to handle a mapping of solution config
206    # to project config.  Should we be able to handle variants being a dict,
207    # or add a separate variant_map variable?  If it's a dict, we can't
208    # guarantee the order of variants since dict keys aren't ordered.
209
210
211    # TODO(rspangler): Automatically write to disk for now; should delay until
212    # node-evaluation time.
213    self.Write()
214
215
216  def Write(self, writer=gyp.common.WriteOnDiff):
217    """Writes the solution file to disk.
218
219    Raises:
220      IndexError: An entry appears multiple times.
221    """
222    # Walk the entry tree and collect all the folders and projects.
223    all_entries = set()
224    entries_to_check = self.entries[:]
225    while entries_to_check:
226      e = entries_to_check.pop(0)
227
228      # If this entry has been visited, nothing to do.
229      if e in all_entries:
230        continue
231
232      all_entries.add(e)
233
234      # If this is a folder, check its entries too.
235      if isinstance(e, MSVSFolder):
236        entries_to_check += e.entries
237
238    all_entries = sorted(all_entries)
239
240    # Open file and print header
241    f = writer(self.path)
242    f.write('Microsoft Visual Studio Solution File, '
243            'Format Version %s\r\n' % self.version.SolutionVersion())
244    f.write('# %s\r\n' % self.version.Description())
245
246    # Project entries
247    sln_root = os.path.split(self.path)[0]
248    for e in all_entries:
249      relative_path = gyp.common.RelativePath(e.path, sln_root)
250      # msbuild does not accept an empty folder_name.
251      # use '.' in case relative_path is empty.
252      folder_name = relative_path.replace('/', '\\') or '.'
253      f.write('Project("%s") = "%s", "%s", "%s"\r\n' % (
254          e.entry_type_guid,          # Entry type GUID
255          e.name,                     # Folder name
256          folder_name,                # Folder name (again)
257          e.get_guid(),               # Entry GUID
258      ))
259
260      # TODO(rspangler): Need a way to configure this stuff
261      if self.websiteProperties:
262        f.write('\tProjectSection(WebsiteProperties) = preProject\r\n'
263                '\t\tDebug.AspNetCompiler.Debug = "True"\r\n'
264                '\t\tRelease.AspNetCompiler.Debug = "False"\r\n'
265                '\tEndProjectSection\r\n')
266
267      if isinstance(e, MSVSFolder):
268        if e.items:
269          f.write('\tProjectSection(SolutionItems) = preProject\r\n')
270          for i in e.items:
271            f.write('\t\t%s = %s\r\n' % (i, i))
272          f.write('\tEndProjectSection\r\n')
273
274      if isinstance(e, MSVSProject):
275        if e.dependencies:
276          f.write('\tProjectSection(ProjectDependencies) = postProject\r\n')
277          for d in e.dependencies:
278            f.write('\t\t%s = %s\r\n' % (d.get_guid(), d.get_guid()))
279          f.write('\tEndProjectSection\r\n')
280
281      f.write('EndProject\r\n')
282
283    # Global section
284    f.write('Global\r\n')
285
286    # Configurations (variants)
287    f.write('\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\r\n')
288    for v in self.variants:
289      f.write('\t\t%s = %s\r\n' % (v, v))
290    f.write('\tEndGlobalSection\r\n')
291
292    # Sort config guids for easier diffing of solution changes.
293    config_guids = []
294    config_guids_overrides = {}
295    for e in all_entries:
296      if isinstance(e, MSVSProject):
297        config_guids.append(e.get_guid())
298        config_guids_overrides[e.get_guid()] = e.config_platform_overrides
299    config_guids.sort()
300
301    f.write('\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\r\n')
302    for g in config_guids:
303      for v in self.variants:
304        nv = config_guids_overrides[g].get(v, v)
305        # Pick which project configuration to build for this solution
306        # configuration.
307        f.write('\t\t%s.%s.ActiveCfg = %s\r\n' % (
308            g,              # Project GUID
309            v,              # Solution build configuration
310            nv,             # Project build config for that solution config
311        ))
312
313        # Enable project in this solution configuration.
314        f.write('\t\t%s.%s.Build.0 = %s\r\n' % (
315            g,              # Project GUID
316            v,              # Solution build configuration
317            nv,             # Project build config for that solution config
318        ))
319    f.write('\tEndGlobalSection\r\n')
320
321    # TODO(rspangler): Should be able to configure this stuff too (though I've
322    # never seen this be any different)
323    f.write('\tGlobalSection(SolutionProperties) = preSolution\r\n')
324    f.write('\t\tHideSolutionNode = FALSE\r\n')
325    f.write('\tEndGlobalSection\r\n')
326
327    # Folder mappings
328    # Omit this section if there are no folders
329    if any([e.entries for e in all_entries if isinstance(e, MSVSFolder)]):
330      f.write('\tGlobalSection(NestedProjects) = preSolution\r\n')
331      for e in all_entries:
332        if not isinstance(e, MSVSFolder):
333          continue        # Does not apply to projects, only folders
334        for subentry in e.entries:
335          f.write('\t\t%s = %s\r\n' % (subentry.get_guid(), e.get_guid()))
336      f.write('\tEndGlobalSection\r\n')
337
338    f.write('EndGlobal\r\n')
339
340    f.close()
341