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