341 lines
12 KiB
Python
341 lines
12 KiB
Python
# Copyright (c) 2012 Google Inc. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""New implementation of Visual Studio project generation."""
|
|
|
|
import os
|
|
import random
|
|
|
|
import gyp.common
|
|
|
|
# hashlib is supplied as of Python 2.5 as the replacement interface for md5
|
|
# and other secure hashes. In 2.6, md5 is deprecated. Import hashlib if
|
|
# available, avoiding a deprecation warning under 2.6. Import md5 otherwise,
|
|
# preserving 2.4 compatibility.
|
|
try:
|
|
import hashlib
|
|
_new_md5 = hashlib.md5
|
|
except ImportError:
|
|
import md5
|
|
_new_md5 = md5.new
|
|
|
|
|
|
# Initialize random number generator
|
|
random.seed()
|
|
|
|
# GUIDs for project types
|
|
ENTRY_TYPE_GUIDS = {
|
|
'project': '{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}',
|
|
'folder': '{2150E333-8FDC-42A3-9474-1A3956D46DE8}',
|
|
}
|
|
|
|
#------------------------------------------------------------------------------
|
|
# Helper functions
|
|
|
|
|
|
def MakeGuid(name, seed='msvs_new'):
|
|
"""Returns a GUID for the specified target name.
|
|
|
|
Args:
|
|
name: Target name.
|
|
seed: Seed for MD5 hash.
|
|
Returns:
|
|
A GUID-line string calculated from the name and seed.
|
|
|
|
This generates something which looks like a GUID, but depends only on the
|
|
name and seed. This means the same name/seed will always generate the same
|
|
GUID, so that projects and solutions which refer to each other can explicitly
|
|
determine the GUID to refer to explicitly. It also means that the GUID will
|
|
not change when the project for a target is rebuilt.
|
|
"""
|
|
# Calculate a MD5 signature for the seed and name.
|
|
d = _new_md5(str(seed) + str(name)).hexdigest().upper()
|
|
# Convert most of the signature to GUID form (discard the rest)
|
|
guid = ('{' + d[:8] + '-' + d[8:12] + '-' + d[12:16] + '-' + d[16:20]
|
|
+ '-' + d[20:32] + '}')
|
|
return guid
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
|
|
class MSVSSolutionEntry(object):
|
|
def __cmp__(self, other):
|
|
# Sort by name then guid (so things are in order on vs2008).
|
|
return cmp((self.name, self.get_guid()), (other.name, other.get_guid()))
|
|
|
|
|
|
class MSVSFolder(MSVSSolutionEntry):
|
|
"""Folder in a Visual Studio project or solution."""
|
|
|
|
def __init__(self, path, name = None, entries = None,
|
|
guid = None, items = None):
|
|
"""Initializes the folder.
|
|
|
|
Args:
|
|
path: Full path to the folder.
|
|
name: Name of the folder.
|
|
entries: List of folder entries to nest inside this folder. May contain
|
|
Folder or Project objects. May be None, if the folder is empty.
|
|
guid: GUID to use for folder, if not None.
|
|
items: List of solution items to include in the folder project. May be
|
|
None, if the folder does not directly contain items.
|
|
"""
|
|
if name:
|
|
self.name = name
|
|
else:
|
|
# Use last layer.
|
|
self.name = os.path.basename(path)
|
|
|
|
self.path = path
|
|
self.guid = guid
|
|
|
|
# Copy passed lists (or set to empty lists)
|
|
self.entries = sorted(list(entries or []))
|
|
self.items = list(items or [])
|
|
|
|
self.entry_type_guid = ENTRY_TYPE_GUIDS['folder']
|
|
|
|
def get_guid(self):
|
|
if self.guid is None:
|
|
# Use consistent guids for folders (so things don't regenerate).
|
|
self.guid = MakeGuid(self.path, seed='msvs_folder')
|
|
return self.guid
|
|
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
|
|
class MSVSProject(MSVSSolutionEntry):
|
|
"""Visual Studio project."""
|
|
|
|
def __init__(self, path, name = None, dependencies = None, guid = None,
|
|
spec = None, build_file = None, config_platform_overrides = None,
|
|
fixpath_prefix = None):
|
|
"""Initializes the project.
|
|
|
|
Args:
|
|
path: Absolute path to the project file.
|
|
name: Name of project. If None, the name will be the same as the base
|
|
name of the project file.
|
|
dependencies: List of other Project objects this project is dependent
|
|
upon, if not None.
|
|
guid: GUID to use for project, if not None.
|
|
spec: Dictionary specifying how to build this project.
|
|
build_file: Filename of the .gyp file that the vcproj file comes from.
|
|
config_platform_overrides: optional dict of configuration platforms to
|
|
used in place of the default for this target.
|
|
fixpath_prefix: the path used to adjust the behavior of _fixpath
|
|
"""
|
|
self.path = path
|
|
self.guid = guid
|
|
self.spec = spec
|
|
self.build_file = build_file
|
|
# Use project filename if name not specified
|
|
self.name = name or os.path.splitext(os.path.basename(path))[0]
|
|
|
|
# Copy passed lists (or set to empty lists)
|
|
self.dependencies = list(dependencies or [])
|
|
|
|
self.entry_type_guid = ENTRY_TYPE_GUIDS['project']
|
|
|
|
if config_platform_overrides:
|
|
self.config_platform_overrides = config_platform_overrides
|
|
else:
|
|
self.config_platform_overrides = {}
|
|
self.fixpath_prefix = fixpath_prefix
|
|
self.msbuild_toolset = None
|
|
|
|
def set_dependencies(self, dependencies):
|
|
self.dependencies = list(dependencies or [])
|
|
|
|
def get_guid(self):
|
|
if self.guid is None:
|
|
# Set GUID from path
|
|
# TODO(rspangler): This is fragile.
|
|
# 1. We can't just use the project filename sans path, since there could
|
|
# be multiple projects with the same base name (for example,
|
|
# foo/unittest.vcproj and bar/unittest.vcproj).
|
|
# 2. The path needs to be relative to $SOURCE_ROOT, so that the project
|
|
# GUID is the same whether it's included from base/base.sln or
|
|
# foo/bar/baz/baz.sln.
|
|
# 3. The GUID needs to be the same each time this builder is invoked, so
|
|
# that we don't need to rebuild the solution when the project changes.
|
|
# 4. We should be able to handle pre-built project files by reading the
|
|
# GUID from the files.
|
|
self.guid = MakeGuid(self.name)
|
|
return self.guid
|
|
|
|
def set_msbuild_toolset(self, msbuild_toolset):
|
|
self.msbuild_toolset = msbuild_toolset
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
|
|
class MSVSSolution(object):
|
|
"""Visual Studio solution."""
|
|
|
|
def __init__(self, path, version, entries=None, variants=None,
|
|
websiteProperties=True):
|
|
"""Initializes the solution.
|
|
|
|
Args:
|
|
path: Path to solution file.
|
|
version: Format version to emit.
|
|
entries: List of entries in solution. May contain Folder or Project
|
|
objects. May be None, if the folder is empty.
|
|
variants: List of build variant strings. If none, a default list will
|
|
be used.
|
|
websiteProperties: Flag to decide if the website properties section
|
|
is generated.
|
|
"""
|
|
self.path = path
|
|
self.websiteProperties = websiteProperties
|
|
self.version = version
|
|
|
|
# Copy passed lists (or set to empty lists)
|
|
self.entries = list(entries or [])
|
|
|
|
if variants:
|
|
# Copy passed list
|
|
self.variants = variants[:]
|
|
else:
|
|
# Use default
|
|
self.variants = ['Debug|Win32', 'Release|Win32']
|
|
# TODO(rspangler): Need to be able to handle a mapping of solution config
|
|
# to project config. Should we be able to handle variants being a dict,
|
|
# or add a separate variant_map variable? If it's a dict, we can't
|
|
# guarantee the order of variants since dict keys aren't ordered.
|
|
|
|
|
|
# TODO(rspangler): Automatically write to disk for now; should delay until
|
|
# node-evaluation time.
|
|
self.Write()
|
|
|
|
|
|
def Write(self, writer=gyp.common.WriteOnDiff):
|
|
"""Writes the solution file to disk.
|
|
|
|
Raises:
|
|
IndexError: An entry appears multiple times.
|
|
"""
|
|
# Walk the entry tree and collect all the folders and projects.
|
|
all_entries = set()
|
|
entries_to_check = self.entries[:]
|
|
while entries_to_check:
|
|
e = entries_to_check.pop(0)
|
|
|
|
# If this entry has been visited, nothing to do.
|
|
if e in all_entries:
|
|
continue
|
|
|
|
all_entries.add(e)
|
|
|
|
# If this is a folder, check its entries too.
|
|
if isinstance(e, MSVSFolder):
|
|
entries_to_check += e.entries
|
|
|
|
all_entries = sorted(all_entries)
|
|
|
|
# Open file and print header
|
|
f = writer(self.path)
|
|
f.write('Microsoft Visual Studio Solution File, '
|
|
'Format Version %s\r\n' % self.version.SolutionVersion())
|
|
f.write('# %s\r\n' % self.version.Description())
|
|
|
|
# Project entries
|
|
sln_root = os.path.split(self.path)[0]
|
|
for e in all_entries:
|
|
relative_path = gyp.common.RelativePath(e.path, sln_root)
|
|
# msbuild does not accept an empty folder_name.
|
|
# use '.' in case relative_path is empty.
|
|
folder_name = relative_path.replace('/', '\\') or '.'
|
|
f.write('Project("%s") = "%s", "%s", "%s"\r\n' % (
|
|
e.entry_type_guid, # Entry type GUID
|
|
e.name, # Folder name
|
|
folder_name, # Folder name (again)
|
|
e.get_guid(), # Entry GUID
|
|
))
|
|
|
|
# TODO(rspangler): Need a way to configure this stuff
|
|
if self.websiteProperties:
|
|
f.write('\tProjectSection(WebsiteProperties) = preProject\r\n'
|
|
'\t\tDebug.AspNetCompiler.Debug = "True"\r\n'
|
|
'\t\tRelease.AspNetCompiler.Debug = "False"\r\n'
|
|
'\tEndProjectSection\r\n')
|
|
|
|
if isinstance(e, MSVSFolder):
|
|
if e.items:
|
|
f.write('\tProjectSection(SolutionItems) = preProject\r\n')
|
|
for i in e.items:
|
|
f.write('\t\t%s = %s\r\n' % (i, i))
|
|
f.write('\tEndProjectSection\r\n')
|
|
|
|
if isinstance(e, MSVSProject):
|
|
if e.dependencies:
|
|
f.write('\tProjectSection(ProjectDependencies) = postProject\r\n')
|
|
for d in e.dependencies:
|
|
f.write('\t\t%s = %s\r\n' % (d.get_guid(), d.get_guid()))
|
|
f.write('\tEndProjectSection\r\n')
|
|
|
|
f.write('EndProject\r\n')
|
|
|
|
# Global section
|
|
f.write('Global\r\n')
|
|
|
|
# Configurations (variants)
|
|
f.write('\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\r\n')
|
|
for v in self.variants:
|
|
f.write('\t\t%s = %s\r\n' % (v, v))
|
|
f.write('\tEndGlobalSection\r\n')
|
|
|
|
# Sort config guids for easier diffing of solution changes.
|
|
config_guids = []
|
|
config_guids_overrides = {}
|
|
for e in all_entries:
|
|
if isinstance(e, MSVSProject):
|
|
config_guids.append(e.get_guid())
|
|
config_guids_overrides[e.get_guid()] = e.config_platform_overrides
|
|
config_guids.sort()
|
|
|
|
f.write('\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\r\n')
|
|
for g in config_guids:
|
|
for v in self.variants:
|
|
nv = config_guids_overrides[g].get(v, v)
|
|
# Pick which project configuration to build for this solution
|
|
# configuration.
|
|
f.write('\t\t%s.%s.ActiveCfg = %s\r\n' % (
|
|
g, # Project GUID
|
|
v, # Solution build configuration
|
|
nv, # Project build config for that solution config
|
|
))
|
|
|
|
# Enable project in this solution configuration.
|
|
f.write('\t\t%s.%s.Build.0 = %s\r\n' % (
|
|
g, # Project GUID
|
|
v, # Solution build configuration
|
|
nv, # Project build config for that solution config
|
|
))
|
|
f.write('\tEndGlobalSection\r\n')
|
|
|
|
# TODO(rspangler): Should be able to configure this stuff too (though I've
|
|
# never seen this be any different)
|
|
f.write('\tGlobalSection(SolutionProperties) = preSolution\r\n')
|
|
f.write('\t\tHideSolutionNode = FALSE\r\n')
|
|
f.write('\tEndGlobalSection\r\n')
|
|
|
|
# Folder mappings
|
|
# Omit this section if there are no folders
|
|
if any([e.entries for e in all_entries if isinstance(e, MSVSFolder)]):
|
|
f.write('\tGlobalSection(NestedProjects) = preSolution\r\n')
|
|
for e in all_entries:
|
|
if not isinstance(e, MSVSFolder):
|
|
continue # Does not apply to projects, only folders
|
|
for subentry in e.entries:
|
|
f.write('\t\t%s = %s\r\n' % (subentry.get_guid(), e.get_guid()))
|
|
f.write('\tEndGlobalSection\r\n')
|
|
|
|
f.write('EndGlobal\r\n')
|
|
|
|
f.close()
|