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 MD5 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 MD5 signature for the seed and name. 48 d = hashlib.md5((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