# # __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 , # 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 , so translate slash. if rest: result.append("\n") result.append(indent) result.append(">") if slash: result.append("\n") result.append(baseIndent + "") 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