db8e8fd7be96300c82efad715cc4c6d09c9f40e4
[scons.git] / src / engine / SCons / Tool / msvs.py
1 """SCons.Tool.msvs
2
3 Tool-specific initialization for Microsoft Visual Studio project files.
4
5 There normally shouldn't be any need to import this module directly.
6 It will usually be imported through the generic SCons.Tool.Tool()
7 selection method.
8
9 """
10
11 #
12 # __COPYRIGHT__
13 #
14 # Permission is hereby granted, free of charge, to any person obtaining
15 # a copy of this software and associated documentation files (the
16 # "Software"), to deal in the Software without restriction, including
17 # without limitation the rights to use, copy, modify, merge, publish,
18 # distribute, sublicense, and/or sell copies of the Software, and to
19 # permit persons to whom the Software is furnished to do so, subject to
20 # the following conditions:
21 #
22 # The above copyright notice and this permission notice shall be included
23 # in all copies or substantial portions of the Software.
24 #
25 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
26 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
27 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 #
33
34 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
35
36 import base64
37 import md5
38 import os.path
39 import pickle
40 import re
41 import string
42 import sys
43
44 import SCons.Builder
45 import SCons.Node.FS
46 import SCons.Platform.win32
47 import SCons.Script.SConscript
48 import SCons.Util
49 import SCons.Warnings
50
51 ##############################################################################
52 # Below here are the classes and functions for generation of
53 # DSP/DSW/SLN/VCPROJ files.
54 ##############################################################################
55
56 def _hexdigest(s):
57     """Return a string as a string of hex characters.
58     """
59     # NOTE:  This routine is a method in the Python 2.0 interface
60     # of the native md5 module, but we want SCons to operate all
61     # the way back to at least Python 1.5.2, which doesn't have it.
62     h = string.hexdigits
63     r = ''
64     for c in s:
65         i = ord(c)
66         r = r + h[(i >> 4) & 0xF] + h[i & 0xF]
67     return r
68
69 def xmlify(s):
70     s = string.replace(s, "&", "&") # do this first
71     s = string.replace(s, "'", "'")
72     s = string.replace(s, '"', """)
73     return s
74
75 external_makefile_guid = '{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}'
76
77 def _generateGUID(slnfile, name):
78     """This generates a dummy GUID for the sln file to use.  It is
79     based on the MD5 signatures of the sln filename plus the name of
80     the project.  It basically just needs to be unique, and not
81     change with each invocation."""
82     solution = _hexdigest(md5.new(str(slnfile)+str(name)).digest()).upper()
83     # convert most of the signature to GUID form (discard the rest)
84     solution = "{" + solution[:8] + "-" + solution[8:12] + "-" + solution[12:16] + "-" + solution[16:20] + "-" + solution[20:32] + "}"
85     return solution
86
87 version_re = re.compile(r'(\d+\.\d+)(.*)')
88
89 def msvs_parse_version(s):
90     """
91     Split a Visual Studio version, which may in fact be something like
92     '7.0Exp', into is version number (returned as a float) and trailing
93     "suite" portion.
94     """
95     num, suite = version_re.match(s).groups()
96     return float(num), suite
97
98 # This is how we re-invoke SCons from inside MSVS Project files.
99 # The problem is that we might have been invoked as either scons.bat
100 # or scons.py.  If we were invoked directly as scons.py, then we could
101 # use sys.argv[0] to find the SCons "executable," but that doesn't work
102 # if we were invoked as scons.bat, which uses "python -c" to execute
103 # things and ends up with "-c" as sys.argv[0].  Consequently, we have
104 # the MSVS Project file invoke SCons the same way that scons.bat does,
105 # which works regardless of how we were invoked.
106 def getExecScriptMain(env, xml=None):
107     scons_home = env.get('SCONS_HOME')
108     if not scons_home and os.environ.has_key('SCONS_LIB_DIR'):
109         scons_home = os.environ['SCONS_LIB_DIR']
110     if scons_home:
111         exec_script_main = "from os.path import join; import sys; sys.path = [ r'%s' ] + sys.path; import SCons.Script; SCons.Script.main()" % scons_home
112     else:
113         version = SCons.__version__
114         exec_script_main = "from os.path import join; import sys; sys.path = [ join(sys.prefix, 'Lib', 'site-packages', 'scons-%(version)s'), join(sys.prefix, 'scons-%(version)s'), join(sys.prefix, 'Lib', 'site-packages', 'scons'), join(sys.prefix, 'scons') ] + sys.path; import SCons.Script; SCons.Script.main()" % locals()
115     if xml:
116         exec_script_main = xmlify(exec_script_main)
117     return exec_script_main
118
119 # The string for the Python executable we tell the Project file to use
120 # is either sys.executable or, if an external PYTHON_ROOT environment
121 # variable exists, $(PYTHON)ROOT\\python.exe (generalized a little to
122 # pluck the actual executable name from sys.executable).
123 try:
124     python_root = os.environ['PYTHON_ROOT']
125 except KeyError:
126     python_executable = sys.executable
127 else:
128     python_executable = os.path.join('$$(PYTHON_ROOT)',
129                                      os.path.split(sys.executable)[1])
130
131 class Config:
132     pass
133
134 def splitFully(path):
135     dir, base = os.path.split(path)
136     if dir and dir != '' and dir != path:
137         return splitFully(dir)+[base]
138     if base == '':
139         return []
140     return [base]
141
142 def makeHierarchy(sources):
143     '''Break a list of files into a hierarchy; for each value, if it is a string,
144        then it is a file.  If it is a dictionary, it is a folder.  The string is
145        the original path of the file.'''
146
147     hierarchy = {}
148     for file in sources:
149         path = splitFully(file)
150         if len(path):
151             dict = hierarchy
152             for part in path[:-1]:
153                 if not dict.has_key(part):
154                     dict[part] = {}
155                 dict = dict[part]
156             dict[path[-1]] = file
157         #else:
158         #    print 'Warning: failed to decompose path for '+str(file)
159     return hierarchy
160
161 class _DSPGenerator:
162     """ Base class for DSP generators """
163
164     srcargs = [
165         'srcs',
166         'incs',
167         'localincs',
168         'resources',
169         'misc']
170
171     def __init__(self, dspfile, source, env):
172         self.dspfile = str(dspfile)
173         try:
174             get_abspath = dspfile.get_abspath
175         except AttributeError:
176             self.dspabs = os.path.abspath(dspfile)
177         else:
178             self.dspabs = get_abspath()
179
180         if not env.has_key('variant'):
181             raise SCons.Errors.InternalError, \
182                   "You must specify a 'variant' argument (i.e. 'Debug' or " +\
183                   "'Release') to create an MSVSProject."
184         elif SCons.Util.is_String(env['variant']):
185             variants = [env['variant']]
186         elif SCons.Util.is_List(env['variant']):
187             variants = env['variant']
188
189         if not env.has_key('buildtarget') or env['buildtarget'] == None:
190             buildtarget = ['']
191         elif SCons.Util.is_String(env['buildtarget']):
192             buildtarget = [env['buildtarget']]
193         elif SCons.Util.is_List(env['buildtarget']):
194             if len(env['buildtarget']) != len(variants):
195                 raise SCons.Errors.InternalError, \
196                     "Sizes of 'buildtarget' and 'variant' lists must be the same."
197             buildtarget = []
198             for bt in env['buildtarget']:
199                 if SCons.Util.is_String(bt):
200                     buildtarget.append(bt)
201                 else:
202                     buildtarget.append(bt.get_abspath())
203         else:
204             buildtarget = [env['buildtarget'].get_abspath()]
205         if len(buildtarget) == 1:
206             bt = buildtarget[0]
207             buildtarget = []
208             for _ in variants:
209                 buildtarget.append(bt)
210
211         if not env.has_key('outdir') or env['outdir'] == None:
212             outdir = ['']
213         elif SCons.Util.is_String(env['outdir']):
214             outdir = [env['outdir']]
215         elif SCons.Util.is_List(env['outdir']):
216             if len(env['outdir']) != len(variants):
217                 raise SCons.Errors.InternalError, \
218                     "Sizes of 'outdir' and 'variant' lists must be the same."
219             outdir = []
220             for s in env['outdir']:
221                 if SCons.Util.is_String(s):
222                     outdir.append(s)
223                 else:
224                     outdir.append(s.get_abspath())
225         else:
226             outdir = [env['outdir'].get_abspath()]
227         if len(outdir) == 1:
228             s = outdir[0]
229             outdir = []
230             for v in variants:
231                 outdir.append(s)
232
233         if not env.has_key('runfile') or env['runfile'] == None:
234             runfile = buildtarget[-1:]
235         elif SCons.Util.is_String(env['runfile']):
236             runfile = [env['runfile']]
237         elif SCons.Util.is_List(env['runfile']):
238             if len(env['runfile']) != len(variants):
239                 raise SCons.Errors.InternalError, \
240                     "Sizes of 'runfile' and 'variant' lists must be the same."
241             runfile = []
242             for s in env['runfile']:
243                 if SCons.Util.is_String(s):
244                     runfile.append(s)
245                 else:
246                     runfile.append(s.get_abspath())
247         else:
248             runfile = [env['runfile'].get_abspath()]
249         if len(runfile) == 1:
250             s = runfile[0]
251             runfile = []
252             for v in variants:
253                 runfile.append(s)
254
255         self.sconscript = env['MSVSSCONSCRIPT']
256
257         cmdargs = env.get('cmdargs', '')
258
259         self.env = env
260
261         if self.env.has_key('name'):
262             self.name = self.env['name']
263         else:
264             self.name = os.path.basename(SCons.Util.splitext(self.dspfile)[0])
265         self.name = self.env.subst(self.name)
266
267         sourcenames = [
268             'Source Files',
269             'Header Files',
270             'Local Headers',
271             'Resource Files',
272             'Other Files']
273
274         self.sources = {}
275         for n in sourcenames:
276             self.sources[n] = []
277
278         self.configs = {}
279
280         self.nokeep = 0
281         if env.has_key('nokeep') and env['variant'] != 0:
282             self.nokeep = 1
283
284         if self.nokeep == 0 and os.path.exists(self.dspabs):
285             self.Parse()
286
287         for t in zip(sourcenames,self.srcargs):
288             if self.env.has_key(t[1]):
289                 if SCons.Util.is_List(self.env[t[1]]):
290                     for i in self.env[t[1]]:
291                         if not i in self.sources[t[0]]:
292                             self.sources[t[0]].append(i)
293                 else:
294                     if not self.env[t[1]] in self.sources[t[0]]:
295                         self.sources[t[0]].append(self.env[t[1]])
296
297         for n in sourcenames:
298             self.sources[n].sort(lambda a, b: cmp(a.lower(), b.lower()))
299
300         def AddConfig(self, variant, buildtarget, outdir, runfile, cmdargs, dspfile=dspfile):
301             config = Config()
302             config.buildtarget = buildtarget
303             config.outdir = outdir
304             config.cmdargs = cmdargs
305             config.runfile = runfile
306
307             match = re.match('(.*)\|(.*)', variant)
308             if match:
309                 config.variant = match.group(1)
310                 config.platform = match.group(2)
311             else:
312                 config.variant = variant
313                 config.platform = 'Win32'
314
315             self.configs[variant] = config
316             print "Adding '" + self.name + ' - ' + config.variant + '|' + config.platform + "' to '" + str(dspfile) + "'"
317
318         for i in range(len(variants)):
319             AddConfig(self, variants[i], buildtarget[i], outdir[i], runfile[i], cmdargs)
320
321         self.platforms = []
322         for key in self.configs.keys():
323             platform = self.configs[key].platform
324             if not platform in self.platforms:
325                 self.platforms.append(platform)
326
327     def Build(self):
328         pass
329
330 V6DSPHeader = """\
331 # Microsoft Developer Studio Project File - Name="%(name)s" - Package Owner=<4>
332 # Microsoft Developer Studio Generated Build File, Format Version 6.00
333 # ** DO NOT EDIT **
334
335 # TARGTYPE "Win32 (x86) External Target" 0x0106
336
337 CFG=%(name)s - Win32 %(confkey)s
338 !MESSAGE This is not a valid makefile. To build this project using NMAKE,
339 !MESSAGE use the Export Makefile command and run
340 !MESSAGE 
341 !MESSAGE NMAKE /f "%(name)s.mak".
342 !MESSAGE 
343 !MESSAGE You can specify a configuration when running NMAKE
344 !MESSAGE by defining the macro CFG on the command line. For example:
345 !MESSAGE 
346 !MESSAGE NMAKE /f "%(name)s.mak" CFG="%(name)s - Win32 %(confkey)s"
347 !MESSAGE 
348 !MESSAGE Possible choices for configuration are:
349 !MESSAGE 
350 """
351
352 class _GenerateV6DSP(_DSPGenerator):
353     """Generates a Project file for MSVS 6.0"""
354
355     def PrintHeader(self):
356         # pick a default config
357         confkeys = self.configs.keys()
358         confkeys.sort()
359
360         name = self.name
361         confkey = confkeys[0]
362
363         self.file.write(V6DSPHeader % locals())
364
365         for kind in confkeys:
366             self.file.write('!MESSAGE "%s - Win32 %s" (based on "Win32 (x86) External Target")\n' % (name, kind))
367
368         self.file.write('!MESSAGE \n\n')
369
370     def PrintProject(self):
371         name = self.name
372         self.file.write('# Begin Project\n'
373                         '# PROP AllowPerConfigDependencies 0\n'
374                         '# PROP Scc_ProjName ""\n'
375                         '# PROP Scc_LocalPath ""\n\n')
376
377         first = 1
378         confkeys = self.configs.keys()
379         confkeys.sort()
380         for kind in confkeys:
381             outdir = self.configs[kind].outdir
382             buildtarget = self.configs[kind].buildtarget
383             if first == 1:
384                 self.file.write('!IF  "$(CFG)" == "%s - Win32 %s"\n\n' % (name, kind))
385                 first = 0
386             else:
387                 self.file.write('\n!ELSEIF  "$(CFG)" == "%s - Win32 %s"\n\n' % (name, kind))
388
389             env_has_buildtarget = self.env.has_key('MSVSBUILDTARGET')
390             if not env_has_buildtarget:
391                 self.env['MSVSBUILDTARGET'] = buildtarget
392
393             # have to write this twice, once with the BASE settings, and once without
394             for base in ("BASE ",""):
395                 self.file.write('# PROP %sUse_MFC 0\n'
396                                 '# PROP %sUse_Debug_Libraries ' % (base, base))
397                 if kind.lower().find('debug') < 0:
398                     self.file.write('0\n')
399                 else:
400                     self.file.write('1\n')
401                 self.file.write('# PROP %sOutput_Dir "%s"\n'
402                                 '# PROP %sIntermediate_Dir "%s"\n' % (base,outdir,base,outdir))
403                 cmd = 'echo Starting SCons && ' + self.env.subst('$MSVSBUILDCOM', 1)
404                 self.file.write('# PROP %sCmd_Line "%s"\n'
405                                 '# PROP %sRebuild_Opt "-c && %s"\n'
406                                 '# PROP %sTarget_File "%s"\n'
407                                 '# PROP %sBsc_Name ""\n'
408                                 '# PROP %sTarget_Dir ""\n'\
409                                 %(base,cmd,base,cmd,base,buildtarget,base,base))
410
411             if not env_has_buildtarget:
412                 del self.env['MSVSBUILDTARGET']
413
414         self.file.write('\n!ENDIF\n\n'
415                         '# Begin Target\n\n')
416         for kind in confkeys:
417             self.file.write('# Name "%s - Win32 %s"\n' % (name,kind))
418         self.file.write('\n')
419         first = 0
420         for kind in confkeys:
421             if first == 0:
422                 self.file.write('!IF  "$(CFG)" == "%s - Win32 %s"\n\n' % (name,kind))
423                 first = 1
424             else:
425                 self.file.write('!ELSEIF  "$(CFG)" == "%s - Win32 %s"\n\n' % (name,kind))
426         self.file.write('!ENDIF \n\n')
427         self.PrintSourceFiles()
428         self.file.write('# End Target\n'
429                         '# End Project\n')
430
431         if self.nokeep == 0:
432             # now we pickle some data and add it to the file -- MSDEV will ignore it.
433             pdata = pickle.dumps(self.configs,1)
434             pdata = base64.encodestring(pdata)
435             self.file.write(pdata + '\n')
436             pdata = pickle.dumps(self.sources,1)
437             pdata = base64.encodestring(pdata)
438             self.file.write(pdata + '\n')
439
440     def PrintSourceFiles(self):
441         categories = {'Source Files': 'cpp|c|cxx|l|y|def|odl|idl|hpj|bat',
442                       'Header Files': 'h|hpp|hxx|hm|inl',
443                       'Local Headers': 'h|hpp|hxx|hm|inl',
444                       'Resource Files': 'r|rc|ico|cur|bmp|dlg|rc2|rct|bin|cnt|rtf|gif|jpg|jpeg|jpe',
445                       'Other Files': ''}
446
447         cats = categories.keys()
448         cats.sort(lambda a, b: cmp(a.lower(), b.lower()))
449         for kind in cats:
450             if not self.sources[kind]:
451                 continue # skip empty groups
452
453             self.file.write('# Begin Group "' + kind + '"\n\n')
454             typelist = categories[kind].replace('|',';')
455             self.file.write('# PROP Default_Filter "' + typelist + '"\n')
456
457             for file in self.sources[kind]:
458                 file = os.path.normpath(file)
459                 self.file.write('# Begin Source File\n\n'
460                                 'SOURCE="' + file + '"\n'
461                                 '# End Source File\n')
462             self.file.write('# End Group\n')
463
464         # add the SConscript file outside of the groups
465         self.file.write('# Begin Source File\n\n'
466                         'SOURCE="' + str(self.sconscript) + '"\n'
467                         '# End Source File\n')
468
469     def Parse(self):
470         try:
471             dspfile = open(self.dspabs,'r')
472         except IOError:
473             return # doesn't exist yet, so can't add anything to configs.
474
475         line = dspfile.readline()
476         while line:
477             if line.find("# End Project") > -1:
478                 break
479             line = dspfile.readline()
480
481         line = dspfile.readline()
482         datas = line
483         while line and line != '\n':
484             line = dspfile.readline()
485             datas = datas + line
486
487         # OK, we've found our little pickled cache of data.
488         try:
489             datas = base64.decodestring(datas)
490             data = pickle.loads(datas)
491         except KeyboardInterrupt:
492             raise
493         except:
494             return # unable to unpickle any data for some reason
495
496         self.configs.update(data)
497
498         data = None
499         line = dspfile.readline()
500         datas = line
501         while line and line != '\n':
502             line = dspfile.readline()
503             datas = datas + line
504
505         # OK, we've found our little pickled cache of data.
506         # it has a "# " in front of it, so we strip that.
507         try:
508             datas = base64.decodestring(datas)
509             data = pickle.loads(datas)
510         except KeyboardInterrupt:
511             raise
512         except:
513             return # unable to unpickle any data for some reason
514
515         self.sources.update(data)
516
517     def Build(self):
518         try:
519             self.file = open(self.dspabs,'w')
520         except IOError, detail:
521             raise SCons.Errors.InternalError, 'Unable to open "' + self.dspabs + '" for writing:' + str(detail)
522         else:
523             self.PrintHeader()
524             self.PrintProject()
525             self.file.close()
526
527 V7DSPHeader = """\
528 <?xml version="1.0" encoding = "%(encoding)s"?>
529 <VisualStudioProject
530 \tProjectType="Visual C++"
531 \tVersion="%(versionstr)s"
532 \tName="%(name)s"
533 %(scc_attrs)s
534 \tKeyword="MakeFileProj">
535 """
536
537 V7DSPConfiguration = """\
538 \t\t<Configuration
539 \t\t\tName="%(variant)s|%(platform)s"
540 \t\t\tOutputDirectory="%(outdir)s"
541 \t\t\tIntermediateDirectory="%(outdir)s"
542 \t\t\tConfigurationType="0"
543 \t\t\tUseOfMFC="0"
544 \t\t\tATLMinimizesCRunTimeLibraryUsage="FALSE">
545 \t\t\t<Tool
546 \t\t\t\tName="VCNMakeTool"
547 \t\t\t\tBuildCommandLine="%(buildcmd)s"
548 \t\t\t\tCleanCommandLine="%(cleancmd)s"
549 \t\t\t\tRebuildCommandLine="%(rebuildcmd)s"
550 \t\t\t\tOutput="%(runfile)s"/>
551 \t\t</Configuration>
552 """
553
554 V8DSPHeader = """\
555 <?xml version="1.0" encoding="%(encoding)s"?>
556 <VisualStudioProject
557 \tProjectType="Visual C++"
558 \tVersion="%(versionstr)s"
559 \tName="%(name)s"
560 %(scc_attrs)s
561 \tRootNamespace="%(name)s"
562 \tKeyword="MakeFileProj">
563 """
564
565 V8DSPConfiguration = """\
566 \t\t<Configuration
567 \t\t\tName="%(variant)s|Win32"
568 \t\t\tConfigurationType="0"
569 \t\t\tUseOfMFC="0"
570 \t\t\tATLMinimizesCRunTimeLibraryUsage="false"
571 \t\t\t>
572 \t\t\t<Tool
573 \t\t\t\tName="VCNMakeTool"
574 \t\t\t\tBuildCommandLine="%(buildcmd)s"
575 \t\t\t\tReBuildCommandLine="%(rebuildcmd)s"
576 \t\t\t\tCleanCommandLine="%(cleancmd)s"
577 \t\t\t\tOutput="%(runfile)s"
578 \t\t\t\tPreprocessorDefinitions=""
579 \t\t\t\tIncludeSearchPath=""
580 \t\t\t\tForcedIncludes=""
581 \t\t\t\tAssemblySearchPath=""
582 \t\t\t\tForcedUsingAssemblies=""
583 \t\t\t\tCompileAsManaged=""
584 \t\t\t/>
585 \t\t</Configuration>
586 """
587 class _GenerateV7DSP(_DSPGenerator):
588     """Generates a Project file for MSVS .NET"""
589
590     def __init__(self, dspfile, source, env):
591         _DSPGenerator.__init__(self, dspfile, source, env)
592         self.version = env['MSVS_VERSION']
593         self.version_num, self.suite = msvs_parse_version(self.version)
594         if self.version_num >= 8.0:
595             self.versionstr = '8.00'
596             self.dspheader = V8DSPHeader
597             self.dspconfiguration = V8DSPConfiguration
598         else:
599             if self.version_num >= 7.1:
600                 self.versionstr = '7.10'
601             else:
602                 self.versionstr = '7.00'
603             self.dspheader = V7DSPHeader
604             self.dspconfiguration = V7DSPConfiguration
605         self.file = None
606
607     def PrintHeader(self):
608         env = self.env
609         versionstr = self.versionstr
610         name = self.name
611         encoding = self.env.subst('$MSVSENCODING')
612         scc_provider = env.get('MSVS_SCC_PROVIDER', '')
613         scc_project_name = env.get('MSVS_SCC_PROJECT_NAME', '')
614         scc_aux_path = env.get('MSVS_SCC_AUX_PATH', '')
615         scc_local_path = env.get('MSVS_SCC_LOCAL_PATH', '')
616         project_guid = env.get('MSVS_PROJECT_GUID', '')
617         if self.version_num >= 8.0 and not project_guid:
618             project_guid = _generateGUID(self.dspfile, '')
619         if scc_provider != '':
620             scc_attrs = ('\tProjectGUID="%s"\n'
621                          '\tSccProjectName="%s"\n'
622                          '\tSccAuxPath="%s"\n'
623                          '\tSccLocalPath="%s"\n'
624                          '\tSccProvider="%s"' % (project_guid, scc_project_name, scc_aux_path, scc_local_path, scc_provider))
625         else:
626             scc_attrs = ('\tProjectGUID="%s"\n'
627                          '\tSccProjectName="%s"\n'
628                          '\tSccLocalPath="%s"' % (project_guid, scc_project_name, scc_local_path))
629
630         self.file.write(self.dspheader % locals())
631
632         self.file.write('\t<Platforms>\n')
633         for platform in self.platforms:
634             self.file.write(
635                         '\t\t<Platform\n'
636                         '\t\t\tName="%s"/>\n' % platform)
637         self.file.write('\t</Platforms>\n')
638
639         if self.version_num >= 8.0:
640             self.file.write('\t<ToolFiles>\n'
641                             '\t</ToolFiles>\n')
642
643     def PrintProject(self):
644         self.file.write('\t<Configurations>\n')
645
646         confkeys = self.configs.keys()
647         confkeys.sort()
648         for kind in confkeys:
649             variant = self.configs[kind].variant
650             platform = self.configs[kind].platform
651             outdir = self.configs[kind].outdir
652             buildtarget = self.configs[kind].buildtarget
653             runfile     = self.configs[kind].runfile
654             cmdargs = self.configs[kind].cmdargs
655
656             env_has_buildtarget = self.env.has_key('MSVSBUILDTARGET')
657             if not env_has_buildtarget:
658                 self.env['MSVSBUILDTARGET'] = buildtarget
659
660             starting = 'echo Starting SCons && '
661             if cmdargs:
662                 cmdargs = ' ' + cmdargs
663             else:
664                 cmdargs = ''
665             buildcmd    = xmlify(starting + self.env.subst('$MSVSBUILDCOM', 1) + cmdargs)
666             rebuildcmd  = xmlify(starting + self.env.subst('$MSVSREBUILDCOM', 1) + cmdargs)
667             cleancmd    = xmlify(starting + self.env.subst('$MSVSCLEANCOM', 1) + cmdargs)
668
669             if not env_has_buildtarget:
670                 del self.env['MSVSBUILDTARGET']
671
672             self.file.write(self.dspconfiguration % locals())
673
674         self.file.write('\t</Configurations>\n')
675
676         if self.version_num >= 7.1:
677             self.file.write('\t<References>\n'
678                             '\t</References>\n')
679
680         self.PrintSourceFiles()
681
682         self.file.write('</VisualStudioProject>\n')
683
684         if self.nokeep == 0:
685             # now we pickle some data and add it to the file -- MSDEV will ignore it.
686             pdata = pickle.dumps(self.configs,1)
687             pdata = base64.encodestring(pdata)
688             self.file.write('<!-- SCons Data:\n' + pdata + '\n')
689             pdata = pickle.dumps(self.sources,1)
690             pdata = base64.encodestring(pdata)
691             self.file.write(pdata + '-->\n')
692
693     def printSources(self, hierarchy, commonprefix):
694         sorteditems = hierarchy.items()
695         sorteditems.sort(lambda a, b: cmp(a[0].lower(), b[0].lower()))
696
697         # First folders, then files
698         for key, value in sorteditems:
699             if SCons.Util.is_Dict(value):
700                 self.file.write('\t\t\t<Filter\n'
701                                 '\t\t\t\tName="%s"\n'
702                                 '\t\t\t\tFilter="">\n' % (key))
703                 self.printSources(value, commonprefix)
704                 self.file.write('\t\t\t</Filter>\n')
705
706         for key, value in sorteditems:
707             if SCons.Util.is_String(value):
708                 file = value
709                 if commonprefix:
710                     file = os.path.join(commonprefix, value)
711                 file = os.path.normpath(file)
712                 self.file.write('\t\t\t<File\n'
713                                 '\t\t\t\tRelativePath="%s">\n'
714                                 '\t\t\t</File>\n' % (file))
715
716     def PrintSourceFiles(self):
717         categories = {'Source Files': 'cpp;c;cxx;l;y;def;odl;idl;hpj;bat',
718                       'Header Files': 'h;hpp;hxx;hm;inl',
719                       'Local Headers': 'h;hpp;hxx;hm;inl',
720                       'Resource Files': 'r;rc;ico;cur;bmp;dlg;rc2;rct;bin;cnt;rtf;gif;jpg;jpeg;jpe',
721                       'Other Files': ''}
722
723         self.file.write('\t<Files>\n')
724
725         cats = categories.keys()
726         cats.sort(lambda a, b: cmp(a.lower(), b.lower()))
727         cats = filter(lambda k, s=self: s.sources[k], cats)
728         for kind in cats:
729             if len(cats) > 1:
730                 self.file.write('\t\t<Filter\n'
731                                 '\t\t\tName="%s"\n'
732                                 '\t\t\tFilter="%s">\n' % (kind, categories[kind]))
733
734             sources = self.sources[kind]
735
736             # First remove any common prefix
737             commonprefix = None
738             if len(sources) > 1:
739                 s = map(os.path.normpath, sources)
740                 # take the dirname because the prefix may include parts
741                 # of the filenames (e.g. if you have 'dir\abcd' and
742                 # 'dir\acde' then the cp will be 'dir\a' )
743                 cp = os.path.dirname( os.path.commonprefix(s) )
744                 if cp and s[0][len(cp)] == os.sep:
745                     # +1 because the filename starts after the separator
746                     sources = map(lambda s, l=len(cp)+1: s[l:], sources)
747                     commonprefix = cp
748             elif len(sources) == 1:
749                 commonprefix = os.path.dirname( sources[0] )
750                 sources[0] = os.path.basename( sources[0] )
751
752             hierarchy = makeHierarchy(sources)
753             self.printSources(hierarchy, commonprefix=commonprefix)
754
755             if len(cats)>1:
756                 self.file.write('\t\t</Filter>\n')
757
758         # add the SConscript file outside of the groups
759         self.file.write('\t\t<File\n'
760                         '\t\t\tRelativePath="%s">\n'
761                         '\t\t</File>\n' % str(self.sconscript))
762
763         self.file.write('\t</Files>\n'
764                         '\t<Globals>\n'
765                         '\t</Globals>\n')
766
767     def Parse(self):
768         try:
769             dspfile = open(self.dspabs,'r')
770         except IOError:
771             return # doesn't exist yet, so can't add anything to configs.
772
773         line = dspfile.readline()
774         while line:
775             if line.find('<!-- SCons Data:') > -1:
776                 break
777             line = dspfile.readline()
778
779         line = dspfile.readline()
780         datas = line
781         while line and line != '\n':
782             line = dspfile.readline()
783             datas = datas + line
784
785         # OK, we've found our little pickled cache of data.
786         try:
787             datas = base64.decodestring(datas)
788             data = pickle.loads(datas)
789         except KeyboardInterrupt:
790             raise
791         except:
792             return # unable to unpickle any data for some reason
793
794         self.configs.update(data)
795
796         data = None
797         line = dspfile.readline()
798         datas = line
799         while line and line != '\n':
800             line = dspfile.readline()
801             datas = datas + line
802
803         # OK, we've found our little pickled cache of data.
804         try:
805             datas = base64.decodestring(datas)
806             data = pickle.loads(datas)
807         except KeyboardInterrupt:
808             raise
809         except:
810             return # unable to unpickle any data for some reason
811
812         self.sources.update(data)
813
814     def Build(self):
815         try:
816             self.file = open(self.dspabs,'w')
817         except IOError, detail:
818             raise SCons.Errors.InternalError, 'Unable to open "' + self.dspabs + '" for writing:' + str(detail)
819         else:
820             self.PrintHeader()
821             self.PrintProject()
822             self.file.close()
823
824 class _DSWGenerator:
825     """ Base class for DSW generators """
826     def __init__(self, dswfile, source, env):
827         self.dswfile = os.path.normpath(str(dswfile))
828         self.env = env
829
830         if not env.has_key('projects'):
831             raise SCons.Errors.UserError, \
832                 "You must specify a 'projects' argument to create an MSVSSolution."
833         projects = env['projects']
834         if not SCons.Util.is_List(projects):
835             raise SCons.Errors.InternalError, \
836                 "The 'projects' argument must be a list of nodes."
837         projects = SCons.Util.flatten(projects)
838         if len(projects) < 1:
839             raise SCons.Errors.UserError, \
840                 "You must specify at least one project to create an MSVSSolution."
841         self.dspfiles = map(str, projects)
842
843         if self.env.has_key('name'):
844             self.name = self.env['name']
845         else:
846             self.name = os.path.basename(SCons.Util.splitext(self.dswfile)[0])
847         self.name = self.env.subst(self.name)
848
849     def Build(self):
850         pass
851
852 class _GenerateV7DSW(_DSWGenerator):
853     """Generates a Solution file for MSVS .NET"""
854     def __init__(self, dswfile, source, env):
855         _DSWGenerator.__init__(self, dswfile, source, env)
856
857         self.file = None
858         self.version = self.env['MSVS_VERSION']
859         self.version_num, self.suite = msvs_parse_version(self.version)
860         self.versionstr = '7.00'
861         if self.version_num >= 8.0:
862             self.versionstr = '9.00'
863         elif self.version_num >= 7.1:
864             self.versionstr = '8.00'
865         if self.version_num >= 8.0:
866             self.versionstr = '9.00'
867
868         if env.has_key('slnguid') and env['slnguid']:
869             self.slnguid = env['slnguid']
870         else:
871             self.slnguid = _generateGUID(dswfile, self.name)
872
873         self.configs = {}
874
875         self.nokeep = 0
876         if env.has_key('nokeep') and env['variant'] != 0:
877             self.nokeep = 1
878
879         if self.nokeep == 0 and os.path.exists(self.dswfile):
880             self.Parse()
881
882         def AddConfig(self, variant, dswfile=dswfile):
883             config = Config()
884
885             match = re.match('(.*)\|(.*)', variant)
886             if match:
887                 config.variant = match.group(1)
888                 config.platform = match.group(2)
889             else:
890                 config.variant = variant
891                 config.platform = 'Win32'
892
893             self.configs[variant] = config
894             print "Adding '" + self.name + ' - ' + config.variant + '|' + config.platform + "' to '" + str(dswfile) + "'"
895
896         if not env.has_key('variant'):
897             raise SCons.Errors.InternalError, \
898                   "You must specify a 'variant' argument (i.e. 'Debug' or " +\
899                   "'Release') to create an MSVS Solution File."
900         elif SCons.Util.is_String(env['variant']):
901             AddConfig(self, env['variant'])
902         elif SCons.Util.is_List(env['variant']):
903             for variant in env['variant']:
904                 AddConfig(self, variant)
905
906         self.platforms = []
907         for key in self.configs.keys():
908             platform = self.configs[key].platform
909             if not platform in self.platforms:
910                 self.platforms.append(platform)
911
912     def Parse(self):
913         try:
914             dswfile = open(self.dswfile,'r')
915         except IOError:
916             return # doesn't exist yet, so can't add anything to configs.
917
918         line = dswfile.readline()
919         while line:
920             if line[:9] == "EndGlobal":
921                 break
922             line = dswfile.readline()
923
924         line = dswfile.readline()
925         datas = line
926         while line:
927             line = dswfile.readline()
928             datas = datas + line
929
930         # OK, we've found our little pickled cache of data.
931         try:
932             datas = base64.decodestring(datas)
933             data = pickle.loads(datas)
934         except KeyboardInterrupt:
935             raise
936         except:
937             return # unable to unpickle any data for some reason
938
939         self.configs.update(data)
940
941     def PrintSolution(self):
942         """Writes a solution file"""
943         self.file.write('Microsoft Visual Studio Solution File, Format Version %s\n' % self.versionstr )
944         if self.version_num >= 8.0:
945             self.file.write('# Visual Studio 2005\n')
946         for p in self.dspfiles:
947             name = os.path.basename(p)
948             base, suffix = SCons.Util.splitext(name)
949             if suffix == '.vcproj':
950                 name = base
951             guid = _generateGUID(p, '')
952             self.file.write('Project("%s") = "%s", "%s", "%s"\n'
953                             % ( external_makefile_guid, name, p, guid ) )
954             if self.version_num >= 7.1 and self.version_num < 8.0:
955                 self.file.write('\tProjectSection(ProjectDependencies) = postProject\n'
956                                 '\tEndProjectSection\n')
957             self.file.write('EndProject\n')
958
959         self.file.write('Global\n')
960
961         env = self.env
962         if env.has_key('MSVS_SCC_PROVIDER'):
963             dspfile_base = os.path.basename(self.dspfile)
964             slnguid = self.slnguid
965             scc_provider = env.get('MSVS_SCC_PROVIDER', '')
966             scc_provider = string.replace(scc_provider, ' ', r'\u0020')
967             scc_project_name = env.get('MSVS_SCC_PROJECT_NAME', '')
968             # scc_aux_path = env.get('MSVS_SCC_AUX_PATH', '')
969             scc_local_path = env.get('MSVS_SCC_LOCAL_PATH', '')
970             scc_project_base_path = env.get('MSVS_SCC_PROJECT_BASE_PATH', '')
971             # project_guid = env.get('MSVS_PROJECT_GUID', '')
972
973             self.file.write('\tGlobalSection(SourceCodeControl) = preSolution\n'
974                             '\t\tSccNumberOfProjects = 2\n'
975                             '\t\tSccProjectUniqueName0 = %(dspfile_base)s\n'
976                             '\t\tSccLocalPath0 = %(scc_local_path)s\n'
977                             '\t\tCanCheckoutShared = true\n'
978                             '\t\tSccProjectFilePathRelativizedFromConnection0 = %(scc_project_base_path)s\n'
979                             '\t\tSccProjectName1 = %(scc_project_name)s\n'
980                             '\t\tSccLocalPath1 = %(scc_local_path)s\n'
981                             '\t\tSccProvider1 = %(scc_provider)s\n'
982                             '\t\tCanCheckoutShared = true\n'
983                             '\t\tSccProjectFilePathRelativizedFromConnection1 = %(scc_project_base_path)s\n'
984                             '\t\tSolutionUniqueID = %(slnguid)s\n'
985                             '\tEndGlobalSection\n' % locals())
986
987         if self.version_num >= 8.0:
988             self.file.write('\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n')
989         else:
990             self.file.write('\tGlobalSection(SolutionConfiguration) = preSolution\n')
991
992         confkeys = self.configs.keys()
993         confkeys.sort()
994         cnt = 0
995         for name in confkeys:
996             variant = self.configs[name].variant
997             platform = self.configs[name].platform
998             if self.version_num >= 8.0:
999                 self.file.write('\t\t%s|%s = %s|%s\n' % (variant, platform, variant, platform))
1000             else:
1001                 self.file.write('\t\tConfigName.%d = %s\n' % (cnt, variant))
1002             cnt = cnt + 1
1003         self.file.write('\tEndGlobalSection\n')
1004         if self.version_num < 7.1:
1005             self.file.write('\tGlobalSection(ProjectDependencies) = postSolution\n'
1006                             '\tEndGlobalSection\n')
1007         if self.version_num >= 8.0:
1008             self.file.write('\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n')
1009         else:
1010             self.file.write('\tGlobalSection(ProjectConfiguration) = postSolution\n')
1011
1012         for name in confkeys:
1013             variant = self.configs[name].variant
1014             platform = self.configs[name].platform
1015             if self.version_num >= 8.0:
1016                 for p in self.dspfiles:
1017                     guid = _generateGUID(p, '')
1018                     self.file.write('\t\t%s.%s|%s.ActiveCfg = %s|%s\n'
1019                                     '\t\t%s.%s|%s.Build.0 = %s|%s\n'  % (guid,variant,platform,variant,platform,guid,variant,platform,variant,platform))
1020             else:
1021                 self.file.write('\t\t%s.%s.ActiveCfg = %s|%s\n'
1022                                 '\t\t%s.%s.Build.0 = %s|%s\n'  %(self.slnguid,variant,variant,platform,self.slnguid,variant,variant,platform))
1023
1024         self.file.write('\tEndGlobalSection\n')
1025
1026         if self.version_num >= 8.0:
1027             self.file.write('\tGlobalSection(SolutionProperties) = preSolution\n'
1028                             '\t\tHideSolutionNode = FALSE\n'
1029                             '\tEndGlobalSection\n')
1030         else:
1031             self.file.write('\tGlobalSection(ExtensibilityGlobals) = postSolution\n'
1032                             '\tEndGlobalSection\n'
1033                             '\tGlobalSection(ExtensibilityAddIns) = postSolution\n'
1034                             '\tEndGlobalSection\n')
1035         self.file.write('EndGlobal\n')
1036         if self.nokeep == 0:
1037             pdata = pickle.dumps(self.configs,1)
1038             pdata = base64.encodestring(pdata)
1039             self.file.write(pdata + '\n')
1040
1041     def Build(self):
1042         try:
1043             self.file = open(self.dswfile,'w')
1044         except IOError, detail:
1045             raise SCons.Errors.InternalError, 'Unable to open "' + self.dswfile + '" for writing:' + str(detail)
1046         else:
1047             self.PrintSolution()
1048             self.file.close()
1049
1050 V6DSWHeader = """\
1051 Microsoft Developer Studio Workspace File, Format Version 6.00
1052 # WARNING: DO NOT EDIT OR DELETE THIS WORKSPACE FILE!
1053
1054 ###############################################################################
1055
1056 Project: "%(name)s"="%(dspfile)s" - Package Owner=<4>
1057
1058 Package=<5>
1059 {{{
1060 }}}
1061
1062 Package=<4>
1063 {{{
1064 }}}
1065
1066 ###############################################################################
1067
1068 Global:
1069
1070 Package=<5>
1071 {{{
1072 }}}
1073
1074 Package=<3>
1075 {{{
1076 }}}
1077
1078 ###############################################################################
1079 """
1080
1081 class _GenerateV6DSW(_DSWGenerator):
1082     """Generates a Workspace file for MSVS 6.0"""
1083
1084     def PrintWorkspace(self):
1085         """ writes a DSW file """
1086         name = self.name
1087         dspfile = self.dspfiles[0]
1088         self.file.write(V6DSWHeader % locals())
1089
1090     def Build(self):
1091         try:
1092             self.file = open(self.dswfile,'w')
1093         except IOError, detail:
1094             raise SCons.Errors.InternalError, 'Unable to open "' + self.dswfile + '" for writing:' + str(detail)
1095         else:
1096             self.PrintWorkspace()
1097             self.file.close()
1098
1099
1100 def GenerateDSP(dspfile, source, env):
1101     """Generates a Project file based on the version of MSVS that is being used"""
1102
1103     version_num = 6.0
1104     if env.has_key('MSVS_VERSION'):
1105         version_num, suite = msvs_parse_version(env['MSVS_VERSION'])
1106     if version_num >= 7.0:
1107         g = _GenerateV7DSP(dspfile, source, env)
1108         g.Build()
1109     else:
1110         g = _GenerateV6DSP(dspfile, source, env)
1111         g.Build()
1112
1113 def GenerateDSW(dswfile, source, env):
1114     """Generates a Solution/Workspace file based on the version of MSVS that is being used"""
1115
1116     version_num = 6.0
1117     if env.has_key('MSVS_VERSION'):
1118         version_num, suite = msvs_parse_version(env['MSVS_VERSION'])
1119     if version_num >= 7.0:
1120         g = _GenerateV7DSW(dswfile, source, env)
1121         g.Build()
1122     else:
1123         g = _GenerateV6DSW(dswfile, source, env)
1124         g.Build()
1125
1126
1127 ##############################################################################
1128 # Above here are the classes and functions for generation of
1129 # DSP/DSW/SLN/VCPROJ files.
1130 ##############################################################################
1131
1132 def get_default_visualstudio_version(env):
1133     """Returns the version set in the env, or the latest version
1134     installed, if it can find it, or '6.0' if all else fails.  Also
1135     updates the environment with what it found."""
1136
1137     versions = ['6.0']
1138
1139     if not env.has_key('MSVS') or not SCons.Util.is_Dict(env['MSVS']):
1140         v = get_visualstudio_versions()
1141         if v:
1142             versions = v
1143         env['MSVS'] = {'VERSIONS' : versions}
1144     else:
1145         versions = env['MSVS'].get('VERSIONS', versions)
1146
1147     if not env.has_key('MSVS_VERSION'):
1148         env['MSVS_VERSION'] = versions[0] #use highest version by default
1149
1150     env['MSVS']['VERSION'] = env['MSVS_VERSION']
1151
1152     return env['MSVS_VERSION']
1153
1154 def get_visualstudio_versions():
1155     """
1156     Get list of visualstudio versions from the Windows registry.
1157     Returns a list of strings containing version numbers.  An empty list
1158     is returned if we were unable to accees the register (for example,
1159     we couldn't import the registry-access module) or the appropriate
1160     registry keys weren't found.
1161     """
1162
1163     if not SCons.Util.can_read_reg:
1164         return []
1165
1166     HLM = SCons.Util.HKEY_LOCAL_MACHINE
1167     KEYS = {
1168         r'Software\Microsoft\VisualStudio'      : '',
1169         r'Software\Microsoft\VCExpress'         : 'Exp',
1170     }
1171     L = []
1172     for K, suite_suffix in KEYS.items():
1173         try:
1174             k = SCons.Util.RegOpenKeyEx(HLM, K)
1175             i = 0
1176             while 1:
1177                 try:
1178                     p = SCons.Util.RegEnumKey(k,i)
1179                 except SCons.Util.RegError:
1180                     break
1181                 i = i + 1
1182                 if not p[0] in '123456789' or p in L:
1183                     continue
1184                 # Only add this version number if there is a valid
1185                 # registry structure (includes the "Setup" key),
1186                 # and at least some of the correct directories
1187                 # exist.  Sometimes VS uninstall leaves around
1188                 # some registry/filesystem turds that we don't
1189                 # want to trip over.  Also, some valid registry
1190                 # entries are MSDN entries, not MSVS ('7.1',
1191                 # notably), and we want to skip those too.
1192                 try:
1193                     SCons.Util.RegOpenKeyEx(HLM, K + '\\' + p + '\\Setup')
1194                 except SCons.Util.RegError:
1195                     continue
1196
1197                 id = []
1198                 idk = SCons.Util.RegOpenKeyEx(HLM, K + '\\' + p)
1199                 # This is not always here -- it only exists if the
1200                 # user installed into a non-standard location (at
1201                 # least in VS6 it works that way -- VS7 seems to
1202                 # always write it)
1203                 try:
1204                     id = SCons.Util.RegQueryValueEx(idk, 'InstallDir')
1205                 except SCons.Util.RegError:
1206                     pass
1207
1208                 # If the InstallDir key doesn't exist,
1209                 # then we check the default locations.
1210                 # Note: The IDE's executable is not devenv.exe for VS8 Express.
1211                 if not id or not id[0]:
1212                     files_dir = SCons.Platform.win32.get_program_files_dir()
1213                     version_num, suite = msvs_parse_version(p)
1214                     if version_num < 7.0:
1215                         vs = r'Microsoft Visual Studio\Common\MSDev98'
1216                     elif version_num < 8.0:
1217                         vs = r'Microsoft Visual Studio .NET\Common7\IDE'
1218                     else:
1219                         vs = r'Microsoft Visual Studio 8\Common7\IDE'
1220                     id = [ os.path.join(files_dir, vs) ]
1221                 if os.path.exists(id[0]):
1222                     L.append(p + suite_suffix)
1223         except SCons.Util.RegError:
1224             pass
1225
1226     if not L:
1227         return []
1228
1229     # This is a hack to get around the fact that certain Visual Studio
1230     # patches place a "6.1" version in the registry, which does not have
1231     # any of the keys we need to find include paths, install directories,
1232     # etc.  Therefore we ignore it if it is there, since it throws all
1233     # other logic off.
1234     try:
1235         L.remove("6.1")
1236     except ValueError:
1237         pass
1238
1239     L.sort()
1240     L.reverse()
1241
1242     return L
1243
1244 def get_default_visualstudio8_suite(env):
1245     """
1246     Returns the Visual Studio 2005 suite identifier set in the env, or the
1247     highest suite installed.
1248     """
1249     if not env.has_key('MSVS') or not SCons.Util.is_Dict(env['MSVS']):
1250         env['MSVS'] = {}
1251
1252     if env.has_key('MSVS_SUITE'):
1253         suite = env['MSVS_SUITE'].upper()
1254         suites = [suite]
1255     else:
1256         suite = 'EXPRESS'
1257         suites = [suite]
1258         if SCons.Util.can_read_reg:
1259             suites = get_visualstudio8_suites()
1260             if suites:
1261                 suite = suites[0] #use best suite by default
1262
1263     env['MSVS_SUITE'] = suite
1264     env['MSVS']['SUITES'] = suites
1265     env['MSVS']['SUITE'] = suite
1266
1267     return suite
1268
1269 def get_visualstudio8_suites():
1270     """
1271     Returns a sorted list of all installed Visual Studio 2005 suites found
1272     in the registry. The highest version should be the first entry in the list.
1273     """
1274
1275     suites = []
1276
1277     # Detect Standard, Professional and Team edition
1278     try:
1279         idk = SCons.Util.RegOpenKeyEx(SCons.Util.HKEY_LOCAL_MACHINE,
1280             r'Software\Microsoft\VisualStudio\8.0')
1281         SCons.Util.RegQueryValueEx(idk, 'InstallDir')
1282         editions = { 'PRO': r'Setup\VS\Pro' }       # ToDo: add standard and team editions
1283         edition_name = 'STD'
1284         for name, key_suffix in editions.items():
1285             try:
1286                 idk = SCons.Util.RegOpenKeyEx(SCons.Util.HKEY_LOCAL_MACHINE,
1287                     r'Software\Microsoft\VisualStudio\8.0' + '\\' + key_suffix )
1288                 edition_name = name
1289             except SCons.Util.RegError:
1290                 pass
1291             suites.append(edition_name)
1292     except SCons.Util.RegError:
1293         pass
1294
1295     # Detect Express edition
1296     try:
1297         idk = SCons.Util.RegOpenKeyEx(SCons.Util.HKEY_LOCAL_MACHINE,
1298             r'Software\Microsoft\VCExpress\8.0')
1299         SCons.Util.RegQueryValueEx(idk, 'InstallDir')
1300         suites.append('EXPRESS')
1301     except SCons.Util.RegError:
1302         pass
1303
1304     return suites
1305
1306 def is_msvs_installed():
1307     """
1308     Check the registry for an installed visual studio.
1309     """
1310     try:
1311         v = SCons.Tool.msvs.get_visualstudio_versions()
1312         return v
1313     except (SCons.Util.RegError, SCons.Errors.InternalError):
1314         return 0
1315
1316 def get_msvs_install_dirs(version = None, vs8suite = None):
1317     """
1318     Get installed locations for various msvc-related products, like the .NET SDK
1319     and the Platform SDK.
1320     """
1321
1322     if not SCons.Util.can_read_reg:
1323         return {}
1324
1325     if not version:
1326         versions = get_visualstudio_versions()
1327         if versions:
1328             version = versions[0] #use highest version by default
1329         else:
1330             return {}
1331
1332     version_num, suite = msvs_parse_version(version)
1333
1334     K = 'Software\\Microsoft\\VisualStudio\\' + str(version_num)
1335     if (version_num >= 8.0):
1336         if vs8suite == None:
1337             # We've been given no guidance about which Visual Studio 8
1338             # suite to use, so attempt to autodetect.
1339             suites = get_visualstudio8_suites()
1340             if suites:
1341                 vs8suite = suites[0]
1342
1343         if vs8suite == 'EXPRESS':
1344             K = 'Software\\Microsoft\\VCExpress\\' + str(version_num)
1345
1346     # vc++ install dir
1347     rv = {}
1348     if (version_num < 7.0):
1349         key = K + r'\Setup\Microsoft Visual C++\ProductDir'
1350     else:
1351         key = K + r'\Setup\VC\ProductDir'
1352     try:
1353         (rv['VCINSTALLDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE, key)
1354     except SCons.Util.RegError:
1355         pass
1356
1357     # visual studio install dir
1358     if (version_num < 7.0):
1359         try:
1360             (rv['VSINSTALLDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE,
1361                                                              K + r'\Setup\Microsoft Visual Studio\ProductDir')
1362         except SCons.Util.RegError:
1363             pass
1364
1365         if not rv.has_key('VSINSTALLDIR') or not rv['VSINSTALLDIR']:
1366             if rv.has_key('VCINSTALLDIR') and rv['VCINSTALLDIR']:
1367                 rv['VSINSTALLDIR'] = os.path.dirname(rv['VCINSTALLDIR'])
1368             else:
1369                 rv['VSINSTALLDIR'] = os.path.join(SCons.Platform.win32.get_program_files_dir(),'Microsoft Visual Studio')
1370     else:
1371         try:
1372             (rv['VSINSTALLDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE,
1373                                                              K + r'\Setup\VS\ProductDir')
1374         except SCons.Util.RegError:
1375             pass
1376
1377     # .NET framework install dir
1378     try:
1379         (rv['FRAMEWORKDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE,
1380             r'Software\Microsoft\.NETFramework\InstallRoot')
1381     except SCons.Util.RegError:
1382         pass
1383
1384     if rv.has_key('FRAMEWORKDIR'):
1385         # try and enumerate the installed versions of the .NET framework.
1386         contents = os.listdir(rv['FRAMEWORKDIR'])
1387         l = re.compile('v[0-9]+.*')
1388         installed_framework_versions = filter(lambda e, l=l: l.match(e), contents)
1389
1390         def versrt(a,b):
1391             # since version numbers aren't really floats...
1392             aa = a[1:]
1393             bb = b[1:]
1394             aal = aa.split('.')
1395             bbl = bb.split('.')
1396             c = int(bbl[0]) - int(aal[0])
1397             if c == 0:
1398                 c = int(bbl[1]) - int(aal[1])
1399                 if c == 0:
1400                     c = int(bbl[2]) - int(aal[2])
1401             return c
1402
1403         installed_framework_versions.sort(versrt)
1404
1405         rv['FRAMEWORKVERSIONS'] = installed_framework_versions
1406
1407         # TODO: allow a specific framework version to be set
1408
1409         # Choose a default framework version based on the Visual
1410         # Studio version.
1411         DefaultFrameworkVersionMap = {
1412             '7.0'   : 'v1.0',
1413             '7.1'   : 'v1.1',
1414             '8.0'   : 'v2.0',
1415             # TODO: Does .NET 3.0 need to be worked into here somewhere?
1416         }
1417         try:
1418             default_framework_version = DefaultFrameworkVersionMap[version[:3]]
1419         except (KeyError, TypeError):
1420             pass
1421         else:
1422             # Look for the first installed directory in FRAMEWORKDIR that
1423             # begins with the framework version string that's appropriate
1424             # for the Visual Studio version we're using.
1425             for v in installed_framework_versions:
1426                 if v[:4] == default_framework_version:
1427                     rv['FRAMEWORKVERSION'] = v
1428                     break
1429
1430         # If the framework version couldn't be worked out by the previous
1431         # code then fall back to using the latest version of the .NET
1432         # framework
1433         if not rv.has_key('FRAMEWORKVERSION'):
1434             rv['FRAMEWORKVERSION'] = installed_framework_versions[0]
1435
1436     # .NET framework SDK install dir
1437     if rv.has_key('FRAMEWORKVERSION'):
1438         # The .NET SDK version used must match the .NET version used,
1439         # so we deliberately don't fall back to other .NET framework SDK
1440         # versions that might be present.
1441         ver = rv['FRAMEWORKVERSION'][:4]
1442         key = r'Software\Microsoft\.NETFramework\sdkInstallRoot' + ver
1443         try:
1444             (rv['FRAMEWORKSDKDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE,
1445                 key)
1446         except SCons.Util.RegError:
1447             pass
1448
1449     # MS Platform SDK dir
1450     try:
1451         (rv['PLATFORMSDKDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE,
1452             r'Software\Microsoft\MicrosoftSDK\Directories\Install Dir')
1453     except SCons.Util.RegError:
1454         pass
1455
1456     if rv.has_key('PLATFORMSDKDIR'):
1457         # if we have a platform SDK, try and get some info on it.
1458         vers = {}
1459         try:
1460             loc = r'Software\Microsoft\MicrosoftSDK\InstalledSDKs'
1461             k = SCons.Util.RegOpenKeyEx(SCons.Util.HKEY_LOCAL_MACHINE,loc)
1462             i = 0
1463             while 1:
1464                 try:
1465                     key = SCons.Util.RegEnumKey(k,i)
1466                     sdk = SCons.Util.RegOpenKeyEx(k,key)
1467                     j = 0
1468                     name = ''
1469                     date = ''
1470                     version = ''
1471                     while 1:
1472                         try:
1473                             (vk,vv,t) = SCons.Util.RegEnumValue(sdk,j)
1474                             if vk.lower() == 'keyword':
1475                                 name = vv
1476                             if vk.lower() == 'propagation_date':
1477                                 date = vv
1478                             if vk.lower() == 'version':
1479                                 version = vv
1480                             j = j + 1
1481                         except SCons.Util.RegError:
1482                             break
1483                     if name:
1484                         vers[name] = (date, version)
1485                     i = i + 1
1486                 except SCons.Util.RegError:
1487                     break
1488             rv['PLATFORMSDK_MODULES'] = vers
1489         except SCons.Util.RegError:
1490             pass
1491
1492     return rv
1493
1494 def GetMSVSProjectSuffix(target, source, env, for_signature):
1495      return env['MSVS']['PROJECTSUFFIX']
1496
1497 def GetMSVSSolutionSuffix(target, source, env, for_signature):
1498      return env['MSVS']['SOLUTIONSUFFIX']
1499
1500 def GenerateProject(target, source, env):
1501     # generate the dsp file, according to the version of MSVS.
1502     builddspfile = target[0]
1503     dspfile = builddspfile.srcnode()
1504
1505     # this detects whether or not we're using a BuildDir
1506     if not dspfile is builddspfile:
1507         try:
1508             bdsp = open(str(builddspfile), "w+")
1509         except IOError, detail:
1510             print 'Unable to open "' + str(dspfile) + '" for writing:',detail,'\n'
1511             raise
1512
1513         bdsp.write("This is just a placeholder file.\nThe real project file is here:\n%s\n" % dspfile.get_abspath())
1514
1515     GenerateDSP(dspfile, source, env)
1516
1517     if env.get('auto_build_solution', 1):
1518         builddswfile = target[1]
1519         dswfile = builddswfile.srcnode()
1520
1521         if not dswfile is builddswfile:
1522
1523             try:
1524                 bdsw = open(str(builddswfile), "w+")
1525             except IOError, detail:
1526                 print 'Unable to open "' + str(dspfile) + '" for writing:',detail,'\n'
1527                 raise
1528
1529             bdsw.write("This is just a placeholder file.\nThe real workspace file is here:\n%s\n" % dswfile.get_abspath())
1530
1531         GenerateDSW(dswfile, source, env)
1532
1533 def GenerateSolution(target, source, env):
1534     GenerateDSW(target[0], source, env)
1535
1536 def projectEmitter(target, source, env):
1537     """Sets up the DSP dependencies."""
1538
1539     # todo: Not sure what sets source to what user has passed as target,
1540     # but this is what happens. When that is fixed, we also won't have
1541     # to make the user always append env['MSVSPROJECTSUFFIX'] to target.
1542     if source[0] == target[0]:
1543         source = []
1544
1545     # make sure the suffix is correct for the version of MSVS we're running.
1546     (base, suff) = SCons.Util.splitext(str(target[0]))
1547     suff = env.subst('$MSVSPROJECTSUFFIX')
1548     target[0] = base + suff
1549
1550     if not source:
1551         source = 'prj_inputs:'
1552         source = source + env.subst('$MSVSSCONSCOM', 1)
1553         source = source + env.subst('$MSVSENCODING', 1)
1554
1555         if env.has_key('buildtarget') and env['buildtarget'] != None:
1556             if SCons.Util.is_String(env['buildtarget']):
1557                 source = source + ' "%s"' % env['buildtarget']
1558             elif SCons.Util.is_List(env['buildtarget']):
1559                 for bt in env['buildtarget']:
1560                     if SCons.Util.is_String(bt):
1561                         source = source + ' "%s"' % bt
1562                     else:
1563                         try: source = source + ' "%s"' % bt.get_abspath()
1564                         except AttributeError: raise SCons.Errors.InternalError, \
1565                             "buildtarget can be a string, a node, a list of strings or nodes, or None"
1566             else:
1567                 try: source = source + ' "%s"' % env['buildtarget'].get_abspath()
1568                 except AttributeError: raise SCons.Errors.InternalError, \
1569                     "buildtarget can be a string, a node, a list of strings or nodes, or None"
1570
1571         if env.has_key('outdir') and env['outdir'] != None:
1572             if SCons.Util.is_String(env['outdir']):
1573                 source = source + ' "%s"' % env['outdir']
1574             elif SCons.Util.is_List(env['outdir']):
1575                 for s in env['outdir']:
1576                     if SCons.Util.is_String(s):
1577                         source = source + ' "%s"' % s
1578                     else:
1579                         try: source = source + ' "%s"' % s.get_abspath()
1580                         except AttributeError: raise SCons.Errors.InternalError, \
1581                             "outdir can be a string, a node, a list of strings or nodes, or None"
1582             else:
1583                 try: source = source + ' "%s"' % env['outdir'].get_abspath()
1584                 except AttributeError: raise SCons.Errors.InternalError, \
1585                     "outdir can be a string, a node, a list of strings or nodes, or None"
1586
1587         if env.has_key('name'):
1588             if SCons.Util.is_String(env['name']):
1589                 source = source + ' "%s"' % env['name']
1590             else:
1591                 raise SCons.Errors.InternalError, "name must be a string"
1592
1593         if env.has_key('variant'):
1594             if SCons.Util.is_String(env['variant']):
1595                 source = source + ' "%s"' % env['variant']
1596             elif SCons.Util.is_List(env['variant']):
1597                 for variant in env['variant']:
1598                     if SCons.Util.is_String(variant):
1599                         source = source + ' "%s"' % variant
1600                     else:
1601                         raise SCons.Errors.InternalError, "name must be a string or a list of strings"
1602             else:
1603                 raise SCons.Errors.InternalError, "variant must be a string or a list of strings"
1604         else:
1605             raise SCons.Errors.InternalError, "variant must be specified"
1606
1607         for s in _DSPGenerator.srcargs:
1608             if env.has_key(s):
1609                 if SCons.Util.is_String(env[s]):
1610                     source = source + ' "%s' % env[s]
1611                 elif SCons.Util.is_List(env[s]):
1612                     for t in env[s]:
1613                         if SCons.Util.is_String(t):
1614                             source = source + ' "%s"' % t
1615                         else:
1616                             raise SCons.Errors.InternalError, s + " must be a string or a list of strings"
1617                 else:
1618                     raise SCons.Errors.InternalError, s + " must be a string or a list of strings"
1619
1620         source = source + ' "%s"' % str(target[0])
1621         source = [SCons.Node.Python.Value(source)]
1622
1623     targetlist = [target[0]]
1624     sourcelist = source
1625
1626     if env.get('auto_build_solution', 1):
1627         env['projects'] = targetlist
1628         t, s = solutionEmitter(target, target, env)
1629         targetlist = targetlist + t
1630
1631     return (targetlist, sourcelist)
1632
1633 def solutionEmitter(target, source, env):
1634     """Sets up the DSW dependencies."""
1635
1636     # todo: Not sure what sets source to what user has passed as target,
1637     # but this is what happens. When that is fixed, we also won't have
1638     # to make the user always append env['MSVSSOLUTIONSUFFIX'] to target.
1639     if source[0] == target[0]:
1640         source = []
1641
1642     # make sure the suffix is correct for the version of MSVS we're running.
1643     (base, suff) = SCons.Util.splitext(str(target[0]))
1644     suff = env.subst('$MSVSSOLUTIONSUFFIX')
1645     target[0] = base + suff
1646
1647     if not source:
1648         source = 'sln_inputs:'
1649
1650         if env.has_key('name'):
1651             if SCons.Util.is_String(env['name']):
1652                 source = source + ' "%s"' % env['name']
1653             else:
1654                 raise SCons.Errors.InternalError, "name must be a string"
1655
1656         if env.has_key('variant'):
1657             if SCons.Util.is_String(env['variant']):
1658                 source = source + ' "%s"' % env['variant']
1659             elif SCons.Util.is_List(env['variant']):
1660                 for variant in env['variant']:
1661                     if SCons.Util.is_String(variant):
1662                         source = source + ' "%s"' % variant
1663                     else:
1664                         raise SCons.Errors.InternalError, "name must be a string or a list of strings"
1665             else:
1666                 raise SCons.Errors.InternalError, "variant must be a string or a list of strings"
1667         else:
1668             raise SCons.Errors.InternalError, "variant must be specified"
1669
1670         if env.has_key('slnguid'):
1671             if SCons.Util.is_String(env['slnguid']):
1672                 source = source + ' "%s"' % env['slnguid']
1673             else:
1674                 raise SCons.Errors.InternalError, "slnguid must be a string"
1675
1676         if env.has_key('projects'):
1677             if SCons.Util.is_String(env['projects']):
1678                 source = source + ' "%s"' % env['projects']
1679             elif SCons.Util.is_List(env['projects']):
1680                 for t in env['projects']:
1681                     if SCons.Util.is_String(t):
1682                         source = source + ' "%s"' % t
1683
1684         source = source + ' "%s"' % str(target[0])
1685         source = [SCons.Node.Python.Value(source)]
1686
1687     return ([target[0]], source)
1688
1689 projectAction = SCons.Action.Action(GenerateProject, None)
1690
1691 solutionAction = SCons.Action.Action(GenerateSolution, None)
1692
1693 projectBuilder = SCons.Builder.Builder(action = '$MSVSPROJECTCOM',
1694                                        suffix = '$MSVSPROJECTSUFFIX',
1695                                        emitter = projectEmitter)
1696
1697 solutionBuilder = SCons.Builder.Builder(action = '$MSVSSOLUTIONCOM',
1698                                         suffix = '$MSVSSOLUTIONSUFFIX',
1699                                         emitter = solutionEmitter)
1700
1701 default_MSVS_SConscript = None
1702
1703 def generate(env):
1704     """Add Builders and construction variables for Microsoft Visual
1705     Studio project files to an Environment."""
1706     try:
1707         env['BUILDERS']['MSVSProject']
1708     except KeyError:
1709         env['BUILDERS']['MSVSProject'] = projectBuilder
1710
1711     try:
1712         env['BUILDERS']['MSVSSolution']
1713     except KeyError:
1714         env['BUILDERS']['MSVSSolution'] = solutionBuilder
1715
1716     env['MSVSPROJECTCOM'] = projectAction
1717     env['MSVSSOLUTIONCOM'] = solutionAction
1718
1719     if SCons.Script.call_stack:
1720         # XXX Need to find a way to abstract this; the build engine
1721         # shouldn't depend on anything in SCons.Script.
1722         env['MSVSSCONSCRIPT'] = SCons.Script.call_stack[0].sconscript
1723     else:
1724         global default_MSVS_SConscript
1725         if default_MSVS_SConscript is None:
1726             default_MSVS_SConscript = env.File('SConstruct')
1727         env['MSVSSCONSCRIPT'] = default_MSVS_SConscript
1728
1729     env['MSVSSCONS'] = '"%s" -c "%s"' % (python_executable, getExecScriptMain(env))
1730     env['MSVSSCONSFLAGS'] = '-C "${MSVSSCONSCRIPT.dir.abspath}" -f ${MSVSSCONSCRIPT.name}'
1731     env['MSVSSCONSCOM'] = '$MSVSSCONS $MSVSSCONSFLAGS'
1732     env['MSVSBUILDCOM'] = '$MSVSSCONSCOM "$MSVSBUILDTARGET"'
1733     env['MSVSREBUILDCOM'] = '$MSVSSCONSCOM "$MSVSBUILDTARGET"'
1734     env['MSVSCLEANCOM'] = '$MSVSSCONSCOM -c "$MSVSBUILDTARGET"'
1735     env['MSVSENCODING'] = 'Windows-1252'
1736
1737     try:
1738         version = get_default_visualstudio_version(env)
1739         # keep a record of some of the MSVS info so the user can use it.
1740         dirs = get_msvs_install_dirs(version)
1741         env['MSVS'].update(dirs)
1742     except (SCons.Util.RegError, SCons.Errors.InternalError):
1743         # we don't care if we can't do this -- if we can't, it's
1744         # because we don't have access to the registry, or because the
1745         # tools aren't installed.  In either case, the user will have to
1746         # find them on their own.
1747         pass
1748
1749     version_num, suite = msvs_parse_version(env['MSVS_VERSION'])
1750     if (version_num < 7.0):
1751         env['MSVS']['PROJECTSUFFIX']  = '.dsp'
1752         env['MSVS']['SOLUTIONSUFFIX'] = '.dsw'
1753     else:
1754         env['MSVS']['PROJECTSUFFIX']  = '.vcproj'
1755         env['MSVS']['SOLUTIONSUFFIX'] = '.sln'
1756
1757     env['GET_MSVSPROJECTSUFFIX']  = GetMSVSProjectSuffix
1758     env['GET_MSVSSOLUTIONSUFFIX']  = GetMSVSSolutionSuffix
1759     env['MSVSPROJECTSUFFIX']  = '${GET_MSVSPROJECTSUFFIX}'
1760     env['MSVSSOLUTIONSUFFIX']  = '${GET_MSVSSOLUTIONSUFFIX}'
1761     env['SCONS_HOME'] = os.environ.get('SCONS_HOME')
1762
1763 def exists(env):
1764     try:
1765         v = SCons.Tool.msvs.get_visualstudio_versions()
1766     except (SCons.Util.RegError, SCons.Errors.InternalError):
1767         pass
1768
1769     if not v:
1770         version_num = 6.0
1771         if env.has_key('MSVS_VERSION'):
1772             version_num, suite = msvs_parse_version(env['MSVS_VERSION'])
1773         if version_num >= 7.0:
1774             # The executable is 'devenv' in Visual Studio Pro,
1775             # Team System and others.  Express Editions have different
1776             # executable names.  Right now we're only going to worry
1777             # about Visual C++ 2005 Express Edition.
1778             return env.Detect('devenv') or env.Detect('vcexpress')
1779         else:
1780             return env.Detect('msdev')
1781     else:
1782         # there's at least one version of MSVS installed.
1783         return 1