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