codekingpro/portable-devtools
114k
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 SHA-256 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 SHA-256 signature for the seed and name.
48 d = hashlib.sha256((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 