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