OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python2.4 |
| 2 # Copyright 2008, Google Inc. |
| 3 # All rights reserved. |
| 4 |
| 5 __doc__ = """SCons.Node.MSVS |
| 6 |
| 7 Microsoft Visual Studio nodes. |
| 8 """ |
| 9 |
| 10 import SCons.Node.FS |
| 11 import SCons.Script |
| 12 |
| 13 |
| 14 """New implementation of Visual Studio project generation for SCons.""" |
| 15 |
| 16 import md5 |
| 17 import os |
| 18 import random |
| 19 |
| 20 |
| 21 # Initialize random number generator |
| 22 random.seed() |
| 23 |
| 24 |
| 25 #------------------------------------------------------------------------------ |
| 26 # Entry point for supplying a fixed map of GUIDs for testing. |
| 27 |
| 28 GUIDMap = {} |
| 29 |
| 30 |
| 31 #------------------------------------------------------------------------------ |
| 32 # Helper functions |
| 33 |
| 34 |
| 35 def 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 = md5.new(str(seed) + str(name)).hexdigest().upper() |
| 52 # Convert most of the signature to GUID form (discard the rest) |
| 53 guid = ('{' + d[:8] + '-' + d[8:12] + '-' + d[12:16] + '-' + d[16:20] |
| 54 + '-' + d[20:32] + '}') |
| 55 return guid |
| 56 |
| 57 |
| 58 #------------------------------------------------------------------------------ |
| 59 # Global look up of string names. |
| 60 |
| 61 class LookupError(Exception): |
| 62 def __str__(self): |
| 63 string, expanded = self.args |
| 64 if string == expanded: |
| 65 return string |
| 66 else: |
| 67 return '%s (%s)' % (string, expanded) |
| 68 |
| 69 _lookup_dict = {} |
| 70 |
| 71 def LookupAdd(item, result): |
| 72 _lookup_dict[item] = result |
| 73 _lookup_dict[result] = result |
| 74 |
| 75 def Lookup(item): |
| 76 """Looks up an MSVS item in the global dictionary. |
| 77 |
| 78 Args: |
| 79 item: A path (string) or instance for looking up. |
| 80 Returns: |
| 81 An instance from the global _lookup_dict. |
| 82 |
| 83 Raises an exception if the item does not exist in the _lookup_dict. |
| 84 """ |
| 85 global _lookup_dict |
| 86 try: |
| 87 return _lookup_dict[item] |
| 88 except KeyError: |
| 89 return SCons.Node.FS.default_fs.Entry(item, create=False) |
| 90 |
| 91 def LookupCreate(klass, item, *args, **kw): |
| 92 """Looks up an MSVS item, creating it if it doesn't already exist. |
| 93 |
| 94 Args: |
| 95 klass: The class of item being looked up, or created if it |
| 96 doesn't already exist in the global _lookup_dict. |
| 97 item: The a string (or instance) being looked up. |
| 98 *args: positional arguments passed to the klass.initialize() method. |
| 99 **kw: keyword arguments passed to the klass.initialize() method. |
| 100 Returns: |
| 101 An instance from the global _lookup_dict, or None if the item does |
| 102 not exist in the _lookup_dict. |
| 103 |
| 104 This raises a LookupError if the found instance doesn't match the |
| 105 requested klass. |
| 106 |
| 107 When creating a new instance, this populates the _lookup_dict with |
| 108 both the item and the instance itself as keys, so that looking up |
| 109 the instance will return itself. |
| 110 """ |
| 111 global _lookup_dict |
| 112 result = _lookup_dict.get(item) |
| 113 if result: |
| 114 if not isinstance(result, klass): |
| 115 raise LookupError, "tried to redefine %s as a %s" % (item, klass) |
| 116 return result |
| 117 result = klass() |
| 118 result.initialize(item, *args, **kw) |
| 119 LookupAdd(item, result) |
| 120 return result |
| 121 |
| 122 |
| 123 #------------------------------------------------------------------------------ |
| 124 |
| 125 class _MSVSFolder(SCons.Node.Node): |
| 126 """Folder in a Visual Studio project or solution.""" |
| 127 |
| 128 entry_type_guid = '{2150E333-8FDC-42A3-9474-1A3956D46DE8}' |
| 129 |
| 130 def initialize(self, path, name = None, entries = None, guid = None, |
| 131 items = None): |
| 132 """Initializes the folder. |
| 133 |
| 134 Args: |
| 135 path: The unique name of the folder, by which other MSVS Nodes can |
| 136 refer to it. This is not necessarily the name that gets printed |
| 137 in the .sln file. |
| 138 name: The name of this folder as actually written in a generated |
| 139 .sln file. The default is |
| 140 entries: List of folder entries to nest inside this folder. May contain |
| 141 Folder or Project objects. May be None, if the folder is empty. |
| 142 guid: GUID to use for folder, if not None. |
| 143 items: List of solution items to include in the folder project. May be |
| 144 None, if the folder does not directly contain items. |
| 145 """ |
| 146 # For folder entries, the path is the same as the name |
| 147 self.msvs_path = path |
| 148 self.msvs_name = name or path |
| 149 |
| 150 self.guid = guid |
| 151 |
| 152 # Copy passed lists (or set to empty lists) |
| 153 self.entries = list(entries or []) |
| 154 self.items = list(items or []) |
| 155 |
| 156 def get_guid(self): |
| 157 if self.guid is None: |
| 158 guid = GUIDMap.get(self.msvs_path) |
| 159 if not guid: |
| 160 # The GUID for the folder can be random, since it's used only inside |
| 161 # solution files and doesn't need to be consistent across runs. |
| 162 guid = MakeGuid(random.random()) |
| 163 self.guid = guid |
| 164 return self.guid |
| 165 |
| 166 def get_msvs_path(self, sln): |
| 167 return self.msvs_name |
| 168 |
| 169 def MSVSFolder(env, item, *args, **kw): |
| 170 return LookupCreate(_MSVSFolder, item, *args, **kw) |
| 171 |
| 172 #------------------------------------------------------------------------------ |
| 173 |
| 174 |
| 175 class _MSVSProject(SCons.Node.FS.File): |
| 176 """Visual Studio project.""" |
| 177 |
| 178 entry_type_guid = '{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}' |
| 179 |
| 180 def initialize(self, path, name = None, dependencies = None, guid = None): |
| 181 """Initializes the project. |
| 182 |
| 183 Args: |
| 184 path: Relative path to project file. |
| 185 name: Name of project. If None, the name will be the same as the base |
| 186 name of the project file. |
| 187 dependencies: List of other Project objects this project is dependent |
| 188 upon, if not None. |
| 189 guid: GUID to use for project, if not None. |
| 190 """ |
| 191 self.msvs_path = path |
| 192 self.msvs_name = name or os.path.splitext(os.path.basename(self.name))[0] |
| 193 |
| 194 self.guid = guid |
| 195 |
| 196 # Copy passed lists (or set to empty lists) |
| 197 self.dependencies = list(dependencies or []) |
| 198 |
| 199 def get_guid(self): |
| 200 if self.guid is None: |
| 201 guid = GUIDMap.get(self.msvs_path) |
| 202 if not guid: |
| 203 # Set GUID from path |
| 204 # TODO(rspangler): This is fragile. |
| 205 # 1. We can't just use the project filename sans path, since there |
| 206 # could be multiple projects with the same base name (for example, |
| 207 # foo/unittest.vcproj and bar/unittest.vcproj). |
| 208 # 2. The path needs to be relative to $SOURCE_ROOT, so that the project |
| 209 # GUID is the same whether it's included from base/base.sln or |
| 210 # foo/bar/baz/baz.sln. |
| 211 # 3. The GUID needs to be the same each time this builder is invoked, |
| 212 # so that we don't need to rebuild the solution when the |
| 213 # project changes. |
| 214 # 4. We should be able to handle pre-built project files by reading the |
| 215 # GUID from the files. |
| 216 guid = MakeGuid(self.msvs_path) |
| 217 self.guid = guid |
| 218 return self.guid |
| 219 |
| 220 def get_msvs_path(self, sln): |
| 221 return sln.rel_path(self).replace('/', '\\') |
| 222 |
| 223 def MSVSProject(env, item, *args, **kw): |
| 224 if not SCons.Util.is_String(item): |
| 225 return item |
| 226 item = env.subst(item) |
| 227 result = env.fs._lookup(item, None, _MSVSProject, create=1) |
| 228 result.initialize(item, *args, **kw) |
| 229 LookupAdd(item, result) |
| 230 return result |
| 231 |
| 232 #------------------------------------------------------------------------------ |
| 233 |
| 234 def MSVSAction(target, source, env): |
| 235 target[0].Write(env) |
| 236 |
| 237 MSVSSolutionAction = SCons.Script.Action(MSVSAction, |
| 238 "Generating Visual Studio solution `$TA
RGET' ...") |
| 239 |
| 240 class _MSVSSolution(SCons.Node.FS.File): |
| 241 """Visual Studio solution.""" |
| 242 |
| 243 def initialize(self, env, path, entries = None, variants = None, |
| 244 websiteProperties = True): |
| 245 """Initializes the solution. |
| 246 |
| 247 Args: |
| 248 path: Path to solution file. |
| 249 entries: List of entries in solution. May contain Folder or Project |
| 250 objects. May be None, if the folder is empty. |
| 251 variants: List of build variant strings. If none, a default list will |
| 252 be used. |
| 253 """ |
| 254 self.msvs_path = path |
| 255 self.websiteProperties = websiteProperties |
| 256 |
| 257 # Copy passed lists (or set to empty lists) |
| 258 self.entries = list(entries or []) |
| 259 |
| 260 if variants: |
| 261 # Copy passed list |
| 262 self.variants = variants[:] |
| 263 else: |
| 264 # Use default |
| 265 self.variants = ['Debug|Win32', 'Release|Win32'] |
| 266 # TODO(rspangler): Need to be able to handle a mapping of solution config |
| 267 # to project config. Should we be able to handle variants being a dict, |
| 268 # or add a separate variant_map variable? If it's a dict, we can't |
| 269 # guarantee the order of variants since dict keys aren't ordered. |
| 270 |
| 271 env.Command(self, [], MSVSSolutionAction) |
| 272 |
| 273 def Write(self, env): |
| 274 """Writes the solution file to disk. |
| 275 |
| 276 Raises: |
| 277 IndexError: An entry appears multiple times. |
| 278 """ |
| 279 r = [] |
| 280 errors = [] |
| 281 |
| 282 def lookup_subst(item, env=env, errors=errors): |
| 283 if SCons.Util.is_String(item): |
| 284 lookup_item = env.subst(item) |
| 285 else: |
| 286 lookup_item = item |
| 287 try: |
| 288 return Lookup(lookup_item) |
| 289 except SCons.Errors.UserError: |
| 290 raise LookupError(item, lookup_item) |
| 291 |
| 292 # Walk the entry tree and collect all the folders and projects. |
| 293 all_entries = [] |
| 294 entries_to_check = self.entries[:] |
| 295 while entries_to_check: |
| 296 # Pop from the beginning of the list to preserve the user's order. |
| 297 entry = entries_to_check.pop(0) |
| 298 try: |
| 299 entry = lookup_subst(entry) |
| 300 except LookupError, e: |
| 301 errors.append("Could not look up entry `%s'." % e) |
| 302 continue |
| 303 |
| 304 # A project or folder can only appear once in the solution's folder tree. |
| 305 # This also protects from cycles. |
| 306 if entry in all_entries: |
| 307 #raise IndexError('Entry "%s" appears more than once in solution' % |
| 308 # e.name) |
| 309 continue |
| 310 |
| 311 all_entries.append(entry) |
| 312 |
| 313 # If this is a folder, check its entries too. |
| 314 if isinstance(entry, _MSVSFolder): |
| 315 entries_to_check += entry.entries |
| 316 |
| 317 # Header |
| 318 r.append('Microsoft Visual Studio Solution File, Format Version 9.00\n') |
| 319 r.append('# Visual Studio 2005\n') |
| 320 |
| 321 # Project entries |
| 322 for e in all_entries: |
| 323 r.append('Project("%s") = "%s", "%s", "%s"\n' % ( |
| 324 e.entry_type_guid, # Entry type GUID |
| 325 e.msvs_name, # Folder name |
| 326 e.get_msvs_path(self), # Folder name (again) |
| 327 e.get_guid(), # Entry GUID |
| 328 )) |
| 329 |
| 330 # TODO(rspangler): Need a way to configure this stuff |
| 331 if self.websiteProperties: |
| 332 r.append('\tProjectSection(WebsiteProperties) = preProject\n' |
| 333 '\t\tDebug.AspNetCompiler.Debug = "True"\n' |
| 334 '\t\tRelease.AspNetCompiler.Debug = "False"\n' |
| 335 '\tEndProjectSection\n') |
| 336 |
| 337 if isinstance(e, _MSVSFolder): |
| 338 if e.items: |
| 339 r.append('\tProjectSection(SolutionItems) = preProject\n') |
| 340 for i in e.items: |
| 341 i = i.replace('/', '\\') |
| 342 r.append('\t\t%s = %s\n' % (i, i)) |
| 343 r.append('\tEndProjectSection\n') |
| 344 |
| 345 if isinstance(e, _MSVSProject): |
| 346 if e.dependencies: |
| 347 r.append('\tProjectSection(ProjectDependencies) = postProject\n') |
| 348 for d in e.dependencies: |
| 349 try: |
| 350 d = lookup_subst(d) |
| 351 except LookupError, e: |
| 352 errors.append("Could not look up dependency `%s'." % e) |
| 353 else: |
| 354 r.append('\t\t%s = %s\n' % (d.get_guid(), d.get_guid())) |
| 355 r.append('\tEndProjectSection\n') |
| 356 |
| 357 r.append('EndProject\n') |
| 358 |
| 359 # Global section |
| 360 r.append('Global\n') |
| 361 |
| 362 # Configurations (variants) |
| 363 r.append('\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n') |
| 364 for v in self.variants: |
| 365 r.append('\t\t%s = %s\n' % (v, v)) |
| 366 r.append('\tEndGlobalSection\n') |
| 367 |
| 368 # Sort config guids for easier diffing of solution changes. |
| 369 config_guids = [] |
| 370 for e in all_entries: |
| 371 if isinstance(e, _MSVSProject): |
| 372 config_guids.append(e.get_guid()) |
| 373 config_guids.sort() |
| 374 |
| 375 r.append('\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n') |
| 376 for g in config_guids: |
| 377 for v in self.variants: |
| 378 r.append('\t\t%s.%s.ActiveCfg = %s\n' % ( |
| 379 g, # Project GUID |
| 380 v, # Solution build configuration |
| 381 v, # Project build config for that solution config |
| 382 )) |
| 383 |
| 384 # Enable project in this solution configuratation |
| 385 r.append('\t\t%s.%s.Build.0 = %s\n' % ( |
| 386 g, # Project GUID |
| 387 v, # Solution build configuration |
| 388 v, # Project build config for that solution config |
| 389 )) |
| 390 r.append('\tEndGlobalSection\n') |
| 391 |
| 392 # TODO(rspangler): Should be able to configure this stuff too (though I've |
| 393 # never seen this be any different) |
| 394 r.append('\tGlobalSection(SolutionProperties) = preSolution\n') |
| 395 r.append('\t\tHideSolutionNode = FALSE\n') |
| 396 r.append('\tEndGlobalSection\n') |
| 397 |
| 398 # Folder mappings |
| 399 # TODO(rspangler): Should omit this section if there are no folders |
| 400 folder_mappings = [] |
| 401 for e in all_entries: |
| 402 if not isinstance(e, _MSVSFolder): |
| 403 continue # Does not apply to projects, only folders |
| 404 for subentry in e.entries: |
| 405 try: |
| 406 subentry = lookup_subst(subentry) |
| 407 except LookupError, e: |
| 408 errors.append("Could not look up subentry `%s'." % subentry) |
| 409 else: |
| 410 folder_mappings.append((subentry.get_guid(), e.get_guid())) |
| 411 folder_mappings.sort() |
| 412 r.append('\tGlobalSection(NestedProjects) = preSolution\n') |
| 413 for fm in folder_mappings: |
| 414 r.append('\t\t%s = %s\n' % fm) |
| 415 r.append('\tEndGlobalSection\n') |
| 416 |
| 417 r.append('EndGlobal\n') |
| 418 |
| 419 if errors: |
| 420 errors = ['Errors while generating solution file:'] + errors |
| 421 raise SCons.Errors.UserError, '\n\t'.join(errors) |
| 422 |
| 423 f = open(self.path, 'wt') |
| 424 f.write(''.join(r)) |
| 425 f.close() |
| 426 |
| 427 def MSVSSolution(env, item, *args, **kw): |
| 428 if not SCons.Util.is_String(item): |
| 429 return item |
| 430 item = env.subst(item) |
| 431 result = env.fs._lookup(item, None, _MSVSSolution, create=1) |
| 432 result.initialize(env, item, *args, **kw) |
| 433 LookupAdd(item, result) |
| 434 return result |
OLD | NEW |