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