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