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