blob: 4b83d56bdb4c4e7eb5ee6881662b5dc8c2a63a7d [file] [log] [blame]
#
# __COPYRIGHT__
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
__doc__ = """SCons.Node.MSVS
New implementation of Visual Studio project generation for SCons.
"""
import md5
import os
import random
import re
import UserList
import xml.dom
import xml.dom.minidom
import SCons.Node.FS
import SCons.Script
import SCons.Util
from SCons.Debug import Trace
TODO = 0
# Initialize random number generator
random.seed()
#------------------------------------------------------------------------------
# Entry point for supplying a fixed map of GUIDs for testing.
GUIDMap = {}
#------------------------------------------------------------------------------
# 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 = md5.new(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
#------------------------------------------------------------------------------
# Global look up of string names.
class LookupError(Exception):
def __str__(self):
string, expanded = self.args
if string == expanded:
return string
else:
return '%s (%s)' % (string, expanded)
_lookup_dict = {}
def LookupAdd(item, result):
_lookup_dict[item] = result
_lookup_dict[result] = result
def Lookup(item):
"""Looks up an MSVS item in the global dictionary.
Args:
item: A path (string) or instance for looking up.
Returns:
An instance from the global _lookup_dict.
Raises an exception if the item does not exist in the _lookup_dict.
"""
global _lookup_dict
try:
return _lookup_dict[item]
except KeyError:
return SCons.Node.FS.default_fs.Entry(item, create=False)
def LookupCreate(klass, item, *args, **kw):
"""Looks up an MSVS item, creating it if it doesn't already exist.
Args:
klass: The class of item being looked up, or created if it
doesn't already exist in the global _lookup_dict.
item: The a string (or instance) being looked up.
*args: positional arguments passed to the klass.initialize() method.
**kw: keyword arguments passed to the klass.initialize() method.
Returns:
An instance from the global _lookup_dict, or None if the item does
not exist in the _lookup_dict.
This raises a LookupError if the found instance doesn't match the
requested klass.
When creating a new instance, this populates the _lookup_dict with
both the item and the instance itself as keys, so that looking up
the instance will return itself.
"""
global _lookup_dict
result = _lookup_dict.get(item)
if result:
if not isinstance(result, klass):
raise LookupError, "tried to redefine %s as a %s" % (item, klass)
return result
result = klass()
result.initialize(item, *args, **kw)
LookupAdd(item, result)
return result
#------------------------------------------------------------------------------
class FileList(object):
def __init__(self, entries=None):
if isinstance(entries, FileList):
entries = entries.entries
self.entries = entries or []
def __getitem__(self, i):
return self.entries[i]
def __setitem__(self, i, item):
self.entries[i] = item
def __delitem__(self, i):
del self.entries[i]
def __add__(self, other):
if isinstance(other, FileList):
return self.__class__(self.entries + other.entries)
elif isinstance(other, type(self.entries)):
return self.__class__(self.entries + other)
else:
return self.__class__(self.entries + list(other))
def __radd__(self, other):
if isinstance(other, FileList):
return self.__class__(other.entries + self.entries)
elif isinstance(other, type(self.entries)):
return self.__class__(other + self.entries)
else:
return self.__class__(list(other) + self.entries)
def __iadd__(self, other):
if isinstance(other, FileList):
self.entries += other.entries
elif isinstance(other, type(self.entries)):
self.entries += other
else:
self.entries += list(other)
return self
def append(self, item):
return self.entries.append(item)
def extend(self, item):
return self.entries.extend(item)
def index(self, item, *args):
return self.entries.index(item, *args)
def remove(self, item):
return self.entries.remove(item)
def FileListWalk(top, topdown=True, onerror=None):
"""
"""
try:
entries = top.entries
except AttributeError, err:
if onerror is not None:
onerror(err)
return
dirs, nondirs = [], []
for entry in entries:
if hasattr(entry, 'entries'):
dirs.append(entry)
else:
nondirs.append(entry)
if topdown:
yield top, dirs, nondirs
for entry in dirs:
for x in FileListWalk(entry, topdown, onerror):
yield x
if not topdown:
yield top, dirs, nondirs
#------------------------------------------------------------------------------
class _MSVSFolder(FileList):
"""Folder in a Visual Studio solution."""
entry_type_guid = '{2150E333-8FDC-42A3-9474-1A3956D46DE8}'
def initialize(self, path, name=None, entries=None, guid=None, items=None):
"""Initializes the folder.
Args:
path: The unique name of the folder, by which other MSVS Nodes can
refer to it. This is not necessarily the name that gets printed
in the .sln file.
name: The name of this folder as actually written in a generated
.sln file. The default is
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.
"""
super(_MSVSFolder, self).__init__(entries)
# For folder entries, the path is the same as the name
self.msvs_path = path
self.msvs_name = name or path
self.guid = guid
# Copy passed lists (or set to empty lists)
self.items = list(items or [])
def get_guid(self):
if self.guid is None:
guid = GUIDMap.get(self.msvs_path)
if not guid:
# The GUID for the folder can be random, since it's used only inside
# solution files and doesn't need to be consistent across runs.
guid = MakeGuid(random.random())
self.guid = guid
return self.guid
def get_msvs_path(self, sln):
return self.msvs_name
def MSVSFolder(env, item, *args, **kw):
return LookupCreate(_MSVSFolder, item, *args, **kw)
#------------------------------------------------------------------------------
class MSVSConfig(object):
"""Visual Studio configuration."""
def __init__(self, Name, config_type, tools=None, **attrs):
"""Initializes the configuration.
Args:
**attrs: Configuration attributes.
"""
# Special handling for attributes that we want to make more
# convenient for the user.
ips = attrs.get('InheritedPropertySheets')
if ips:
if isinstance(ips, list):
ips = ';'.join(ips)
attrs['InheritedPropertySheets'] = ips.replace('/', '\\')
self.Name = Name
self.config_type = config_type
self.tools = tools
self.attrs = attrs
def CreateElement(self, doc, project):
"""Creates an element for the configuration.
Args:
doc: xml.dom.Document object to use for node creation.
Returns:
A new xml.dom.Element for the configuration.
"""
node = doc.createElement(self.config_type)
node.setAttribute('Name', self.Name)
for k, v in self.attrs.items():
node.setAttribute(k, v)
tools = self.tools
if tools is None:
tools = project.tools or []
if not SCons.Util.is_List(tools):
tools = [tools]
tool_objects = []
for t in tools:
if not isinstance(t, MSVSTool):
t = MSVSTool(t)
tool_objects.append(t)
for t in tool_objects:
node.appendChild(t.CreateElement(doc))
return node
class MSVSFileListBase(FileList):
"""Base class for a file list in a Visual Studio project file."""
def CreateElement(self, doc, node_func=lambda x: x):
"""Creates an element for an MSVSFileListBase subclass.
Args:
doc: xml.dom.Document object to use for node creation.
node_func: Function to use to return Nodes for objects that
don't have a CreateElement() method of their own.
Returns:
A new xml.dom.Element for the MSVSFileListBase object.
"""
node = doc.createElement(self.element_name)
for entry in self.entries:
if hasattr(entry, 'CreateElement'):
n = entry.CreateElement(doc, node_func)
else:
n = node_func(entry)
node.appendChild(n)
return node
class MSVSFiles(MSVSFileListBase):
"""Files list in a Visual Studio project file."""
element_name = 'Files'
class MSVSFilter(MSVSFileListBase):
"""Filter (that is, a virtual folder) in a Visual Studio project file."""
element_name = 'Filter'
def __init__(self, Name, entries=None):
"""Initializes the folder.
Args:
Name: Filter (folder) name.
entries: List of filenames and/or Filter objects contained.
"""
super(MSVSFilter, self).__init__(entries)
self.Name = Name
def CreateElement(self, doc, node_func=lambda x: x):
"""Creates an element for the Filter.
Args:
doc: xml.dom.Document object to use for node creation.
node_func: Function to use to return Nodes for objects that
don't have a CreateElement() method of their own.
Returns:
A new xml.dom.Element for the filter.
"""
node = super(MSVSFilter, self).CreateElement(doc, node_func)
node.setAttribute('Name', self.Name)
return node
class MSVSTool(object):
"""Visual Studio tool."""
def __init__(self, Name, **attrs):
"""Initializes the tool.
Args:
Name: Tool name.
**attrs: Tool attributes.
"""
val = attrs.get('AdditionalDependencies')
if val:
if isinstance(val, list):
val = ' '.join(val)
attrs['AdditionalDependencies'] = val.replace('/', '\\')
val = attrs.get('AdditionalIncludeDirectories')
if val:
if isinstance(val, list):
val = ';'.join(val)
attrs['AdditionalIncludeDirectories'] = val.replace('/', '\\')
val = attrs.get('AdditionalManifestFiles')
if val:
if isinstance(val, list):
val = ';'.join(val)
attrs['AdditionalManifestFiles'] = val.replace('/', '\\')
val = attrs.get('CommandLine')
if val:
if isinstance(val, list):
val = '\r\n'.join(val)
attrs['CommandLine'] = val.replace('/', '\\')
val = attrs.get('PreprocessorDefinitions')
if val:
if isinstance(val, list):
val = ';'.join(val)
attrs['PreprocessorDefinitions'] = val
for a in ('ImportLibrary',
'ObjectFile',
'OutputFile',
'Outputs',
'XMLDocumentationFileName'):
val = attrs.get(a)
if val:
val = val.replace('/', '\\')
attrs[a] = val
self.Name = Name
self.attrs = attrs
def CreateElement(self, doc):
"""Creates an element for the tool.
Args:
doc: xml.dom.Document object to use for node creation.
Returns:
A new xml.dom.Element for the tool.
"""
node = doc.createElement('Tool')
node.setAttribute('Name', self.Name)
for k, v in self.attrs.items():
node.setAttribute(k, v)
return node
def _format(self):
"""Formats a tool specification for debug printing"""
xml_impl = xml.dom.getDOMImplementation()
doc = xml_impl.createDocument(None, 'VisualStudioProject', None)
return self.CreateElement(doc).toprettyxml()
def diff(self, other):
for key, value in self.attrs.items():
if other.attrs[key] == value:
del self.attrs[key]
class MSVSToolFile(object):
"""Visual Studio tool file specification."""
def __init__(self, node, **attrs):
"""Initializes the tool.
Args:
node: Node for the Tool File
**attrs: Tool File attributes.
"""
self.node = node
def CreateElement(self, doc, project):
result = doc.createElement('ToolFile')
result.setAttribute('RelativePath', project.get_rel_path(self.node))
return result
#------------------------------------------------------------------------------
def MSVSAction(target, source, env):
target[0].Write(env)
MSVSProjectAction = SCons.Script.Action(MSVSAction,
"Generating Visual Studio project `$TARGET' ...")
class _MSVSProject(SCons.Node.FS.File):
"""Visual Studio project."""
entry_type_guid = '{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}'
initialized = False
def initialize(self, env, path, name = None,
dependencies = None,
guid = None,
buildtargets = [],
files = [],
root_namespace = None,
keyword = None,
relative_path_prefix = None,
local_directory_prefix = None,
relative_path_substitutions = [],
tools = None,
configurations = None,
**attrs):
"""Initializes the project.
Args:
path: Relative path to 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.
buildtargets: List of target(s) being built by this project.
files: List of source files for the project. This will be
supplemented by any source files of buildtargets.
root_namespace: The value of the RootNamespace attribute of the
project, if not None. The default is to use the same
string as the name.
relative_path_prefix: A prefix to be appended to the beginning of
every file name in the list. The canonical use is to specify
'./' to make files explicitly relative to the local directory.
tools: A list of MSVSTool objects or strings representing
tools to be used to build this project. This will be used
for any configurations that don't provide their own
per-configuration tool list.
configurations: A list of MSVSConfig objects representing
configurations built by this project.
"""
if name is None:
if buildtargets:
name = os.path.splitext(buildtargets[0].name)[0]
else:
name = os.path.splitext(os.path.basename(path))[0]
if not root_namespace:
root_namespace or name
if self.initialized:
# TODO(sgk): fill in
if self.msvs_name != name:
pass
if self.root_namespace != root_namespace:
pass
if self.relative_path_prefix != relative_path_prefix:
pass
if self.guid != guid:
pass
#if self.env != env:
# pass
else:
self.buildtargets = []
self.configurations = []
self.dependencies = []
self.file_configurations = {}
self.files = MSVSFiles([])
self.tool_files = []
self.file_lists = []
self.initialized = True
self.keyword = None
self.local_directory_prefix = ''
self.relative_path_prefix = ''
self.relative_path_substitutions = []
self.root_namespace = name
self.attrs = attrs
self.env = env
self.guid = guid
self.msvs_name = name
self.msvs_path = path
if relative_path_prefix:
self.relative_path_prefix = relative_path_prefix
if local_directory_prefix:
self.local_directory_prefix = local_directory_prefix
for left, right in relative_path_substitutions:
t = (left.replace('/', '\\'), right.replace('/', '\\'))
self.relative_path_substitutions.append(t)
if root_namespace:
self.root_namespace = root_namespace
if keyword:
self.keyword = keyword
self.tools = tools
self.buildtargets.extend(buildtargets)
self.configurations.extend(configurations or [])
self.dependencies.extend(list(dependencies or []))
self.AddFiles(files)
env.Command(self, [], MSVSProjectAction)
def args2nodes(self, entries):
result = []
for entry in entries:
if SCons.Util.is_String(entry):
entry = self.env.File(entry)
result.append(entry.srcnode())
elif hasattr(entry, 'entries'):
entry.entries = self.args2nodes(entry.entries)
result.append(entry)
elif isinstance(entry, (list, UserList.UserList)):
result.extend(self.args2nodes(entry))
elif hasattr(entry, 'sources') and entry.sources:
result.extend(entry.sources)
else:
result.append(entry.srcnode())
return result
def FindFile(self, node):
try:
flat_file_dict = self.flat_file_dict
except AttributeError:
flat_file_dict = {}
file_list = self.files[:]
while file_list:
entry = file_list.pop(0)
if not isinstance(entry, (list, UserList.UserList)):
entry = [entry]
for f in entry:
if hasattr(f, 'entries'):
file_list.extend(f.entries)
else:
flat_file_dict[f] = True
flat_file_dict[f.srcnode()] = True
if hasattr(f, 'sources'):
for s in f.sources:
flat_file_dict[s] = True
flat_file_dict[s.srcnode()] = True
self.flat_file_dict = flat_file_dict
return flat_file_dict.get(node)
def get_guid(self):
if self.guid is None:
guid = GUIDMap.get(self.msvs_path)
if not guid:
# 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.
guid = MakeGuid(self.msvs_path)
self.guid = guid
return self.guid
def get_msvs_path(self, sln):
return sln.rel_path(self).replace('/', '\\')
def get_rel_path(self, node):
result = self.rel_path(node)
if self.relative_path_prefix:
if not result.startswith('..'):
result = self.relative_path_prefix + result
elif not os.path.split(result)[0]:
result = self.local_directory_prefix + result
result = result.replace('/', '\\')
for left, right in self.relative_path_substitutions:
result = result.replace(left, right)
return result
def AddConfig(self, Name, tools=None, **attrs):
"""Adds a configuration to the parent node.
Args:
Name: The name of the configuration.
tools: List of tools (strings or Tool objects); may be None.
**attrs: Configuration attributes.
"""
if tools is None:
# No tool list specifically for this configuration,
# use the Project's as a default.
tools = self.tools
if not attrs.has_key('ConfigurationType'):
# No ConfigurationType specifically for this configuration,
# use the Project's as a default.
try:
attrs['ConfigurationType'] = self.attrs['ConfigurationType']
except KeyError:
pass
if attrs.has_key('InheritedPropertySheets'):
ips = attrs['InheritedPropertySheets']
attrs['InheritedPropertySheets'] = self.env.subst(ips)
c = MSVSConfig(Name, 'Configuration', tools=tools, **attrs)
self.configurations.append(c)
def AddFiles(self, files):
"""Adds files to the project.
Args:
files: A list of Filter objects and/or relative paths to files.
This makes a copy of the file/filter tree at the time of this call. If you
later add files to a Filter object which was passed into a previous call
to AddFiles(), it will not be reflected in this project.
"""
self.file_lists.append(self.args2nodes(files))
def _FilesToSourceFiles(self, files):
file_list = files[:]
result = []
while file_list:
entry = file_list.pop(0)
if not isinstance(entry, (list, UserList.UserList)):
entry = [entry]
for f in entry:
if hasattr(f, 'entries'):
self._FilesToSourceFiles(f.entries)
result.append(f)
else:
if f.sources:
flist = f.sources
else:
flist = [f]
for x in flist:
result.append(x.srcnode())
files[:] = result
def _MergeFiles(self, dest_list, src_list):
for f in src_list:
if f not in dest_list:
dest_list.append(f)
continue
#if hasattr(f, 'entries'):
# self._FilesToSourceFiles(f.entries)
def AddFileConfig(self, path, Name, tools=None, **attrs):
"""Adds a configuration to a file.
Args:
path: Relative path to the file.
Name: Name of configuration to add.
tools: List of tools (strings or MSVSTool objects); may be None.
**attrs: Configuration attributes.
Raises:
ValueError: Relative path does not match any file added via AddFiles().
"""
# Store as the VariantDir node, not as the source node.
node = self.env.File(path)
c = MSVSConfig(Name, 'FileConfiguration', tools=tools, **attrs)
config_list = self.file_configurations.get(node)
if config_list is None:
config_list = []
self.file_configurations[node] = config_list
config_list.append(c)
def AddToolFile(self, path):
"""Adds a tool file to the project.
Args:
path: Relative path from project to tool file.
"""
tf = MSVSToolFile(self.env.File(path))
self.tool_files.append(tf)
def Create(self):
"""Creates the project document.
Args:
name: Name of the project.
guid: GUID to use for project, if not None.
"""
# Create XML doc
xml_impl = xml.dom.getDOMImplementation()
self.doc = xml_impl.createDocument(None, 'VisualStudioProject', None)
# Add attributes to root element
root = self.doc.documentElement
root.setAttribute('ProjectType', 'Visual C++')
root.setAttribute('Version', '8.00')
root.setAttribute('Name', self.msvs_name)
root.setAttribute('ProjectGUID', self.get_guid())
root.setAttribute('RootNamespace', self.root_namespace)
if self.keyword:
root.setAttribute('Keyword', self.keyword)
# Add platform list
platforms = self.doc.createElement('Platforms')
root.appendChild(platforms)
n = self.doc.createElement('Platform')
n.setAttribute('Name', 'Win32')
platforms.appendChild(n)
# Add tool files section
tool_files = self.doc.createElement('ToolFiles')
root.appendChild(tool_files)
for tf in self.tool_files:
tool_files.appendChild(tf.CreateElement(self.doc, self))
# Add configurations section
configs = self.doc.createElement('Configurations')
root.appendChild(configs)
for c in self.configurations:
configs.appendChild(c.CreateElement(self.doc, self))
# Add empty References section
root.appendChild(self.doc.createElement('References'))
# Add files section
root.appendChild(self.files.CreateElement(self.doc, self.CreateFileElement))
# Add empty Globals section
root.appendChild(self.doc.createElement('Globals'))
def CreateFileElement(self, file):
"""Create a DOM node for the specified file.
Args:
file: The file Node being considered.
Returns:
A DOM Node for the File, with a relative path to the current
project object, and any file configurations attached to the
project.
"""
node = self.doc.createElement('File')
node.setAttribute('RelativePath', self.get_rel_path(file))
for c in self.file_configurations.get(file, []):
node.appendChild(c.CreateElement(self.doc, self))
return node
def VCCLCompilerTool(self, args):
default_attrs = {
'BufferSecurityCheck' : "false",
'CompileAs' : 0, # default
'DebugInformationFormat' : 0, # TODO(???)
'DisableSpecificWarnings' : [],
'EnableFiberSafeOptimizations' : "false",
'EnableFunctionLevelLinking' : "false",
'EnableIntrinsicFunctions' : "false",
'FavorSizeOrSpeed' : 0, # favorNone
'InlineFunctionExpansion' : 1, # expandDisable
'MinimalRebuild' : "false",
'OmitFramePointers' : "false",
'Optimization' : 1, # optimizeDisabled TODO(???)
'PreprocessorDefinitions' : [],
'RuntimeLibrary' : TODO,
'RuntimeTypeInfo' : "false",
'StringPooling' : "false",
'SuppressStartupBanner' : "false",
'WarningAsError' : "false",
'WarningLevel' : 1, # warningLevel_1
'WholeProgramOptimization' : "false",
}
tool = MSVSTool('VCCLCompilerTool', **default_attrs)
attrs = tool.attrs
for arg in args:
if arg in ('/c',):
continue
if arg.startswith('/Fo'):
continue
if arg.startswith('/D'):
attrs['PreprocessorDefinitions'].append(arg[2:])
elif arg == '/EH':
attrs['ExceptionHandling'] = 0
elif arg == '/GF':
attrs['StringPooling'] = "true"
elif arg == '/GL':
attrs['WholeProgramOptimization'] = "true"
elif arg == '/GM':
attrs['MinimalRebuild'] = "true"
elif arg == '/GR-':
attrs['RuntimeTypeInfo'] = "true"
elif arg == '/Gs':
attrs['BufferSecurityCheck'] = "true"
elif arg == '/Gs-':
attrs['BufferSecurityCheck'] = "false"
elif arg == '/GT':
attrs['EnableFiberSafeOptimizations'] = "true"
elif arg == '/Gy':
attrs['EnableFunctionLevelLinking'] = "true"
elif arg == '/MD':
attrs['RuntimeLibrary'] = 1 # rtMultiThreadedDebug
elif arg == '/MDd':
attrs['RuntimeLibrary'] = 2 # rtMultiThreadedDebugDLL
elif arg == '/MT':
attrs['RuntimeLibrary'] = 0 # rtMultiThreaded
elif arg == '/MTd':
attrs['RuntimeLibrary'] = 3 # rtMultiThreadedDLL
elif arg == '/nologo':
attrs['SuppressStartupBanner'] = "true"
elif arg == '/O1':
attrs['InlineFunctionExpansion'] = 4 # optimizeMinSpace
elif arg == '/O2':
attrs['InlineFunctionExpansion'] = 3 # optimizeMaxSpeed
elif arg == '/Ob1':
attrs['InlineFunctionExpansion'] = 2 # expandOnlyInline
elif arg == '/Ob2':
attrs['InlineFunctionExpansion'] = 0 # expandAnySuitable
elif arg == '/Od':
attrs['Optimization'] = 0
elif arg == '/Oi':
attrs['EnableIntrinsicFunctions'] = "true"
elif arg == '/Os':
attrs['FavorSizeOrSpeed'] = 1 # favorSize
elif arg == '/Ot':
attrs['FavorSizeOrSpeed'] = 2 # favorSpeed
elif arg == '/Ox':
attrs['Optimization'] = 2 # optimizeFull
elif arg == '/Oy':
attrs['OmitFramePointers'] = "true"
elif arg == '/Oy-':
attrs['TODO'] = "true"
elif arg in ('/Tc', '/TC'):
attrs['CompileAs'] = 1 # compileAsC
elif arg in ('/Tp', '/TP'):
attrs['CompileAs'] = 2 # compileAsCPlusPlus
elif arg == '/WX':
attrs['WarnAsError'] = "true"
elif arg.startswith('/W'):
attrs['WarningLevel'] = int(arg[2:]) # 0 through 4
elif arg.startswith('/wd'):
attrs['DisableSpecificWarnings'].append(str(arg[3:]))
elif arg == '/Z7':
attrs['DebugInformationFormat'] = 3 # debugOldSytleInfo TODO(???)
elif arg == '/Zd':
attrs['DebugInformationFormat'] = 0 # debugDisabled
elif arg == '/Zi':
attrs['DebugInformationFormat'] = 2 # debugEnabled TODO(???)
elif arg == '/ZI':
attrs['DebugInformationFormat'] = 1 # debugEditAndContinue TODO(???)
cppdefines = attrs['PreprocessorDefinitions']
if cppdefines:
attrs['PreprocessorDefinitions'] = ';'.join(cppdefines)
warnings = attrs['DisableSpecificWarnings']
if warnings:
warnings = SCons.Util.uniquer(warnings)
attrs['DisableSpecificWarnings'] = ';'.join(warnings)
return tool
def VCLibrarianTool(self, args):
default_attrs = {
'LinkTimeCodeGeneration' : "false",
'SuppressStartupBanner' : "false",
}
tool = MSVSTool('VCLibrarianTool', **default_attrs)
attrs = tool.attrs
for arg in args:
if arg.startswith('/OUT'):
continue
if arg == '/ltcg':
attrs['LinkTimeCodeGeneration'] = "true"
elif arg == '/nologo':
attrs['SuppressStartupBanner'] = "true"
return tool
def VCLinkerTool(self, args):
default_attrs = {
'LinkIncremental' : "false",
'LinkTimeCodeGeneration' : "false",
'EnableCOMDATFolding' : TODO,
'OptimizeForWindows98' : TODO,
'OptimizeReferences' : TODO,
'Profile' : "false",
'SuppressStartupBanner' : "false",
}
tool = MSVSTool('VCLinkerTool', **default_attrs)
attrs = tool.attrs
for arg in args:
if arg == '':
continue
if arg == '/INCREMENTAL':
attrs['LinkIncremental'] = "true"
elif arg == '/INCREMENTAL:NO':
attrs['LinkIncremental'] = "false"
elif arg == '/LTCG':
attrs['LinkTimeCodeGeneration'] = "true"
elif arg == '/nologo':
attrs['SuppressStartupBanner'] = "true"
elif arg == '/OPT:NOICF':
attrs['EnableCOMDATFolding'] = 2 #
elif arg == '/OPT:NOWIN98':
attrs['OptimizeForWindows98'] = 1 #
elif arg == '/OPT:REF':
attrs['OptimizeReferences'] = 2 #
elif arg == '/PROFILE':
attrs['Profile'] = "true"
return tool
command_to_tool_map = {
'cl' : 'VCCLCompilerTool',
'cl.exe' : 'VCCLCompilerTool',
'lib' : 'VCLibrarianTool',
'lib.exe' : 'VCLibrarianTool',
'link' : 'VCLinkerTool',
'link.exe' : 'VCLinkerTool',
}
def cl_to_tool(self, args):
command = os.path.basename(args[0])
method_name = self.command_to_tool_map.get(command)
if not method_name:
return None
return getattr(self, method_name)(args[1:])
def _AddFileConfigurationDifferences(self, target, source, base_env, file_env, name):
"""Adds a per-file configuration.
Args:
target: The target being built from the source.
source: The source to which the file configuration is being added.
base_env: The base construction environment for the project.
Differences from this will go into the FileConfiguration
in the project file.
file_env: The construction environment for the target, containing
the per-target settings.
"""
executor = target.get_executor()
base_cl = map(str, base_env.subst_list(executor)[0])
file_cl = map(str, file_env.subst_list(executor)[0])
if base_cl == file_cl:
return
base_tool = self.cl_to_tool(base_cl)
file_tool = self.cl_to_tool(file_cl)
if not base_tool or not_file_tool:
return
file_tool.diff(base_tool)
self.AddFileConfig(source, name, tools=[file_tool])
def _AddFileConfigurations(self, env):
"""Adds per-file configurations for the buildtarget's sources.
Args:
env: The base construction environment for the project.
"""
if not self.buildtargets:
return
for bt in self.buildtargets:
executor = bt.get_executor()
build_env = bt.get_build_env()
bt_cl = map(str, build_env.subst_list(executor)[0])
tool = self.cl_to_tool(bt_cl)
default_tool = self.cl_to_tool([bt_cl[0]])
if default_tool:
tool.diff(default_tool)
else:
# TODO(sgk): print a message unconditionally is too
# verbose for things like Python function actions,
# but doing nothing runs the risk of burying problems.
# Work out a better solution.
#print "no tool for %r" % bt_cl[0]
pass
for t in bt.sources:
e = t.get_build_env()
additional_files = SCons.Util.UniqueList()
for s in t.sources:
s = env.arg2nodes([s])[0].srcnode()
if not self.FindFile(s):
additional_files.append(s)
if not build_env is e:
# TODO(sgk): This test may be bogus, but it works for now.
# We're trying to figure out if the file configuration
# differences need to be added one per build target, or one
# per configuration for the entire project. The assumption
# is that if the number of buildtargets configured matches
# the number of project configurations, that we use those
# in preference to the project configurations.
if len(self.buildtargets) == len(self.configurations):
self._AddFileConfigurationDifferences(t, s, build_env, e, e.subst('$MSVSCONFIGURATIONNAME'))
else:
for config in self.configurations:
self._AddFileConfigurationDifferences(t, s, build_env, e, config.Name)
self._MergeFiles(self.files, additional_files)
def Write(self, env):
"""Writes the project file."""
for flist in self.file_lists:
self._FilesToSourceFiles(flist)
self._MergeFiles(self.files, flist)
for k, v in self.file_configurations.items():
self.file_configurations[str(k)] = v
k = self.env.File(k).srcnode()
self.file_configurations[k] = v
self.file_configurations[str(k)] = v
self._AddFileConfigurations(env)
self.Create()
f = open(str(self), 'wt')
f.write(self.formatMSVSProjectXML(self.doc))
f.close()
# Methods for formatting XML as nearly identically to Microsoft's
# .vcproj output as we can practically make it.
#
# The general design here is copied from:
#
# Bruce Eckels' MindView, Inc: 12-09-04 XML Oddyssey
# http://www.mindview.net/WebLog/log-0068
#
# Eckels' implementation broke up long tag definitions for readability,
# in much the same way that .vcproj does, but we've modified things
# for .vcproj quirks (like some tags *always* terminating with </Tag>,
# even when empty).
encoding = 'Windows-1252'
def formatMSVSProjectXML(self, xmldoc):
xmldoc = xmldoc.toprettyxml("", "\n", encoding=self.encoding)
# Remove trailing whitespace from each line:
xmldoc = "\n".join(
[line.rstrip() for line in xmldoc.split("\n")])
# Remove all empty lines before opening '<':
while xmldoc.find("\n\n<") != -1:
xmldoc = xmldoc.replace("\n\n<", "\n<")
dom = xml.dom.minidom.parseString(xmldoc)
xmldoc = dom.toprettyxml("\t", "", encoding=self.encoding)
xmldoc = xmldoc.replace('?><', '?>\n<')
xmldoc = self.reformatLines(xmldoc)
return xmldoc
def reformatLines(self, xmldoc):
result = []
for line in [line.rstrip() for line in xmldoc.split("\n")]:
if line.lstrip().startswith("<"):
result.append(self.reformatLine(line) + "\n")
else:
result.append(line + "\n")
return ''.join(result)
# Keyword order for specific tags.
#
# Listed keywords will come first and in the specified order.
# Any unlisted keywords come after, in whatever order they appear
# in the input config. In theory this means we would only *have* to
# list the keywords that we care about, but in practice we'll probably
# want to nail down Visual Studio's order to make sure we match them
# as nearly as possible.
order = {
'Configuration' : [
'Name',
'ConfigurationType',
'InheritedPropertySheets',
],
'FileConfiguration' : [
'Name',
'ExcludedFromBuild',
],
'Tool' : [
'Name',
'DisableSpecificWarnings',
'AdditionalIncludeDirectories',
'Description',
'CommandLine',
'OutputFile',
'ImportLibrary',
'PreprocessorDefinitions',
'UsePrecompiledHeader',
'PrecompiledHeaderThrough',
'ForcedIncludeFiles',
],
'VisualStudioProject' : [
'ProjectType',
'Version',
'Name',
'ProjectGUID',
'RootNamespace',
'Keyword',
],
}
force_closing_tag = [
'File',
'Globals',
'References',
'ToolFiles'
]
oneLiner = re.compile("(\s*)<(\w+)(.*)>")
keyValuePair = re.compile('\w+="[^"]*?"')
def reformatLine(self, line):
"""Reformat an xml tag to put each key-value
element on a single indented line, for readability"""
matchobj = self.oneLiner.match(line.rstrip())
if not matchobj:
return line
baseIndent, tag, rest = matchobj.groups()
slash = ''
if rest[-1:] == '/':
slash = '/'
rest = rest[:-1]
result = [baseIndent + '<' + tag]
indent = baseIndent + "\t"
pairs = self.keyValuePair.findall(rest)
for key in self.order.get(tag, []):
for p in [ p for p in pairs if p.startswith(key+'=') ]:
result.append("\n" + indent + p)
pairs.remove(p)
for pair in pairs:
result.append("\n" + indent + pair)
result = [''.join(result).rstrip()]
if tag in self.force_closing_tag:
# These force termination with </Tag>, so translate slash.
if rest:
result.append("\n")
result.append(indent)
result.append(">")
if slash:
result.append("\n")
result.append(baseIndent + "</" + tag + ">")
else:
if rest:
result.append("\n")
if slash:
result.append(baseIndent)
else:
result.append(indent)
result.append(slash + ">")
return ''.join(result)
def MSVSProject(env, item, *args, **kw):
if not SCons.Util.is_String(item):
return item
item = env.subst(item)
result = env.fs._lookup(item, None, _MSVSProject, create=1)
result.initialize(env, item, *args, **kw)
LookupAdd(item, result)
return result
#------------------------------------------------------------------------------
MSVSSolutionAction = SCons.Script.Action(MSVSAction,
"Generating Visual Studio solution `$TARGET' ...")
class _MSVSSolution(SCons.Node.FS.File):
"""Visual Studio solution."""
def initialize(self, env, path, entries=None, variants=None, websiteProperties=True):
"""Initializes the solution.
Args:
path: Path to solution file.
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.
"""
self.msvs_path = path
self.websiteProperties = websiteProperties
# 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.
env.Command(self, [], MSVSSolutionAction)
def Write(self, env):
"""Writes the solution file to disk.
Raises:
IndexError: An entry appears multiple times.
"""
r = []
errors = []
def lookup_subst(item, env=env, errors=errors):
if SCons.Util.is_String(item):
lookup_item = env.subst(item)
else:
lookup_item = item
try:
return Lookup(lookup_item)
except SCons.Errors.UserError:
raise LookupError(item, lookup_item)
# Walk the entry tree and collect all the folders and projects.
all_entries = []
entries_to_check = self.entries[:]
while entries_to_check:
# Pop from the beginning of the list to preserve the user's order.
entry = entries_to_check.pop(0)
try:
entry = lookup_subst(entry)
except LookupError, e:
errors.append("Could not look up entry `%s'." % e)
continue
# A project or folder can only appear once in the solution's folder tree.
# This also protects from cycles.
if entry in all_entries:
#raise IndexError('Entry "%s" appears more than once in solution' %
# e.name)
continue
all_entries.append(entry)
# If this is a folder, check its entries too.
if isinstance(entry, _MSVSFolder):
entries_to_check += entry.entries
# Header
r.append('Microsoft Visual Studio Solution File, Format Version 9.00\n')
r.append('# Visual Studio 2005\n')
# Project entries
for e in all_entries:
r.append('Project("%s") = "%s", "%s", "%s"\n' % (
e.entry_type_guid, # Entry type GUID
e.msvs_name, # Folder name
e.get_msvs_path(self), # Folder name (again)
e.get_guid(), # Entry GUID
))
# TODO(rspangler): Need a way to configure this stuff
if self.websiteProperties:
r.append('\tProjectSection(WebsiteProperties) = preProject\n'
'\t\tDebug.AspNetCompiler.Debug = "True"\n'
'\t\tRelease.AspNetCompiler.Debug = "False"\n'
'\tEndProjectSection\n')
if isinstance(e, _MSVSFolder):
if e.items:
r.append('\tProjectSection(SolutionItems) = preProject\n')
for i in e.items:
i = i.replace('/', '\\')
r.append('\t\t%s = %s\n' % (i, i))
r.append('\tEndProjectSection\n')
if isinstance(e, _MSVSProject):
if e.dependencies:
r.append('\tProjectSection(ProjectDependencies) = postProject\n')
for d in e.dependencies:
try:
d = lookup_subst(d)
except LookupError, e:
errors.append("Could not look up dependency `%s'." % e)
else:
r.append('\t\t%s = %s\n' % (d.get_guid(), d.get_guid()))
r.append('\tEndProjectSection\n')
r.append('EndProject\n')
# Global section
r.append('Global\n')
# Configurations (variants)
r.append('\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n')
for v in self.variants:
r.append('\t\t%s = %s\n' % (v, v))
r.append('\tEndGlobalSection\n')
# Sort config guids for easier diffing of solution changes.
config_guids = []
for e in all_entries:
if isinstance(e, _MSVSProject):
config_guids.append(e.get_guid())
config_guids.sort()
r.append('\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n')
for g in config_guids:
for v in self.variants:
r.append('\t\t%s.%s.ActiveCfg = %s\n' % (
g, # Project GUID
v, # Solution build configuration
v, # Project build config for that solution config
))
# Enable project in this solution configuratation
r.append('\t\t%s.%s.Build.0 = %s\n' % (
g, # Project GUID
v, # Solution build configuration
v, # Project build config for that solution config
))
r.append('\tEndGlobalSection\n')
# TODO(rspangler): Should be able to configure this stuff too (though I've
# never seen this be any different)
r.append('\tGlobalSection(SolutionProperties) = preSolution\n')
r.append('\t\tHideSolutionNode = FALSE\n')
r.append('\tEndGlobalSection\n')
# Folder mappings
# TODO(rspangler): Should omit this section if there are no folders
folder_mappings = []
for e in all_entries:
if not isinstance(e, _MSVSFolder):
continue # Does not apply to projects, only folders
for subentry in e.entries:
try:
subentry = lookup_subst(subentry)
except LookupError, e:
errors.append("Could not look up subentry `%s'." % subentry)
else:
folder_mappings.append((subentry.get_guid(), e.get_guid()))
folder_mappings.sort()
r.append('\tGlobalSection(NestedProjects) = preSolution\n')
for fm in folder_mappings:
r.append('\t\t%s = %s\n' % fm)
r.append('\tEndGlobalSection\n')
r.append('EndGlobal\n')
if errors:
errors = ['Errors while generating solution file:'] + errors
raise SCons.Errors.UserError, '\n\t'.join(errors)
f = open(self.path, 'wt')
f.write(''.join(r))
f.close()
def MSVSSolution(env, item, *args, **kw):
if not SCons.Util.is_String(item):
return item
item = env.subst(item)
result = env.fs._lookup(item, None, _MSVSSolution, create=1)
result.initialize(env, item, *args, **kw)
LookupAdd(item, result)
return result
class Derived(SCons.Util.Proxy):
def srcnode(self, *args, **kw):
return self
def __getattr__(self, name):
if name == 'sources':
return []
return SCons.Util.Proxy.__getattr__(self, name)
def __hash__(self, *args, **kw):
return id(self)
import __builtin__
__builtin__.Derived = Derived
__builtin__.MSVSConfig = MSVSConfig
__builtin__.MSVSFilter = MSVSFilter
__builtin__.MSVSProject = MSVSProject
__builtin__.MSVSSolution = MSVSSolution
__builtin__.MSVSTool = MSVSTool