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