fb4f1b3a5e6effab78f61fdfc013e5edd71e810d
[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 import types
44
45 import SCons.Builder
46 import SCons.Node.FS
47 import SCons.Platform.win32
48 import SCons.Script
49 import SCons.Util
50 import SCons.Warnings
51
52 ##############################################################################
53 # Below here are the classes and functions for generation of
54 # DSP/DSW/SLN/VCPROJ files.
55 ##############################################################################
56
57 def _hexdigest(s):
58     """Return a string as a string of hex characters.
59     """
60     # NOTE:  This routine is a method in the Python 2.0 interface
61     # of the native md5 module, but we want SCons to operate all
62     # the way back to at least Python 1.5.2, which doesn't have it.
63     h = string.hexdigits
64     r = ''
65     for c in s:
66         i = ord(c)
67         r = r + h[(i >> 4) & 0xF] + h[i & 0xF]
68     return r
69
70 def _generateGUID(slnfile, name):
71     """This generates a dummy GUID for the sln file to use.  It is
72     based on the MD5 signatures of the sln filename plus the name of
73     the project.  It basically just needs to be unique, and not
74     change with each invocation."""
75     solution = _hexdigest(md5.new(str(slnfile)+str(name)).digest()).upper()
76     # convert most of the signature to GUID form (discard the rest)
77     solution = "{" + solution[:8] + "-" + solution[8:12] + "-" + solution[12:16] + "-" + solution[16:20] + "-" + solution[20:32] + "}"
78     return solution
79
80 # This is how we re-invoke SCons from inside MSVS Project files.
81 # The problem is that we might have been invoked as either scons.bat
82 # or scons.py.  If we were invoked directly as scons.py, then we could
83 # use sys.argv[0] to find the SCons "executable," but that doesn't work
84 # if we were invoked as scons.bat, which uses "python -c" to execute
85 # things and ends up with "-c" as sys.argv[0].  Consequently, we have
86 # the MSVS Project file invoke SCons the same way that scons.bat does,
87 # which works regardless of how we were invoked.
88 exec_script_main = "from os.path import join; import sys; sys.path = [ join(sys.prefix, 'Lib', 'site-packages', 'scons-__VERSION__'), join(sys.prefix, 'scons-__VERSION__'), join(sys.prefix, 'Lib', 'site-packages', 'scons'), join(sys.prefix, 'scons') ] + sys.path; import SCons.Script; SCons.Script.main()"
89 exec_script_main_xml = string.replace(exec_script_main, "'", "'")
90
91 # The string for the Python executable we tell the Project file to use
92 # is either sys.executable or, if an external PYTHON_ROOT environment
93 # variable exists, $(PYTHON)ROOT\\python.exe (generalized a little to
94 # pluck the actual executable name from sys.executable).
95 try:
96     python_root = os.environ['PYTHON_ROOT']
97 except KeyError:
98     python_executable = sys.executable
99 else:
100     python_executable = os.path.join('$(PYTHON_ROOT)',
101                                      os.path.split(sys.executable)[1])
102
103 class Config:
104     pass
105
106 class _DSPGenerator:
107     """ Base class for DSP generators """
108     def __init__(self, dspfile, source, env):
109         if type(dspfile) == types.StringType:
110             self.dspfile = os.path.abspath(dspfile)
111         else:
112             self.dspfile = dspfile.get_abspath()
113
114         try:
115             self.conspath = source[0].attributes.sconstruct.get_abspath()
116         except KeyError:
117             raise SCons.Errors.InternalError, \
118                   "Unable to determine where the SConstruct is"
119
120         self.config = Config()
121         if env.has_key('variant'):
122             self.config.variant = env['variant'].capitalize()
123         else:
124             raise SCons.Errors.InternalError, \
125                   "You must specify a 'variant' argument (i.e. 'Debug' or " +\
126                   "'Release') to create an MSVSProject."
127
128         if env.has_key('buildtarget'):
129             if type(env['buildtarget']) == types.StringType:
130                 self.config.buildtarget = os.path.abspath(env['buildtarget'])
131             elif type(env['buildtarget']) == types.ListType:
132                 self.config.buildtarget = env['buildtarget'][0].get_abspath()
133             else:
134                 self.config.buildtarget = env['buildtarget'].get_abspath()
135         else:
136             raise SCons.Errors.InternalError, \
137                   "You must specify a target 'buildtarget' file argument (such as the target" +\
138                   " executable) to create an MSVSProject."
139
140         self.config.outdir = os.path.dirname(self.config.buildtarget)
141
142         if type(source[0]) == types.StringType:
143             self.source = os.path.abspath(source[0])
144         else:
145             self.source = source[0].get_abspath()
146
147         self.env = env
148
149         if self.env.has_key('name'):
150             self.name = self.env['name']
151         else:
152             self.name = os.path.basename(SCons.Util.splitext(self.dspfile)[0])
153
154         print "Adding '" + self.name + ' - ' + self.config.variant + "' to Visual Studio Project '" + str(dspfile) + "'"
155
156         sourcenames = [
157             ' Source Files',
158             'Header Files',
159             'Local Headers',
160             'Resource Files',
161             'Other Files']
162
163         srcargs = [
164             'srcs',
165             'incs',
166             'localincs',
167             'resources',
168             'misc']
169
170         self.sources = {}
171         for n in sourcenames:
172             self.sources[n] = []
173
174         self.configs = {}
175
176         if os.path.exists(self.dspfile):
177             self.Parse()
178
179         for t in zip(sourcenames,srcargs):
180             if self.env.has_key(t[1]):
181                 if type(self.env[t[1]]) == types.ListType:
182                     for i in self.env[t[1]]:
183                         if not i in self.sources[t[0]]:
184                             self.sources[t[0]].append(i)
185                 else:
186                     if not self.env[t[1]] in self.sources[t[0]]:
187                         self.sources[t[0]].append(self.env[t[1]])
188
189         for n in sourcenames:
190             self.sources[n].sort()
191
192         self.configs[self.config.variant] = self.config
193
194     def Build(self):
195         pass
196
197 V6DSPHeader = """\
198 # Microsoft Developer Studio Project File - Name="%(name)s" - Package Owner=<4>
199 # Microsoft Developer Studio Generated Build File, Format Version 6.00
200 # ** DO NOT EDIT **
201
202 # TARGTYPE "Win32 (x86) External Target" 0x0106
203
204 CFG=%(name)s - Win32 %(confkey)s
205 !MESSAGE This is not a valid makefile. To build this project using NMAKE,
206 !MESSAGE use the Export Makefile command and run
207 !MESSAGE 
208 !MESSAGE NMAKE /f "%(name)s.mak".
209 !MESSAGE 
210 !MESSAGE You can specify a configuration when running NMAKE
211 !MESSAGE by defining the macro CFG on the command line. For example:
212 !MESSAGE 
213 !MESSAGE NMAKE /f "%(name)s.mak" CFG="%(name)s - Win32 %(confkey)s"
214 !MESSAGE 
215 !MESSAGE Possible choices for configuration are:
216 !MESSAGE 
217 """
218
219 class _GenerateV6DSP(_DSPGenerator):
220     """Generates a Project file for MSVS 6.0"""
221
222     def PrintHeader(self):
223         # pick a default config
224         confkeys = self.configs.keys()
225         confkeys.sort()
226
227         name = self.name
228         confkey = confkeys[0]
229
230         self.file.write(V6DSPHeader % locals())
231
232         for kind in confkeys:
233             self.file.write('!MESSAGE "%s - Win32 %s" (based on "Win32 (x86) External Target")\n' % (name, kind))
234
235         self.file.write('!MESSAGE \n\n')
236
237     def PrintProject(self):
238         name = self.name
239         self.file.write('# Begin Project\n'
240                         '# PROP AllowPerConfigDependencies 0\n'
241                         '# PROP Scc_ProjName ""\n'
242                         '# PROP Scc_LocalPath ""\n\n')
243
244         first = 1
245         confkeys = self.configs.keys()
246         confkeys.sort()
247         for kind in confkeys:
248             outdir = self.configs[kind].outdir
249             buildtarget = self.configs[kind].buildtarget
250             if first == 1:
251                 self.file.write('!IF  "$(CFG)" == "%s - Win32 %s"\n\n' % (name, kind))
252                 first = 0
253             else:
254                 self.file.write('\n!ELSEIF  "$(CFG)" == "%s - Win32 %s"\n\n' % (name, kind))
255
256             # have to write this twice, once with the BASE settings, and once without
257             for base in ("BASE ",""):
258                 self.file.write('# PROP %sUse_MFC 0\n'
259                                 '# PROP %sUse_Debug_Libraries ' % (base, base))
260                 if kind.lower().find('debug') < 0:
261                     self.file.write('0\n')
262                 else:
263                     self.file.write('1\n')
264                 self.file.write('# PROP %sOutput_Dir "%s"\n'
265                                 '# PROP %sIntermediate_Dir "%s"\n' % (base,outdir,base,outdir))
266                 (d,c) = os.path.split(str(self.conspath))
267                 cmd = 'echo Starting SCons && "%s" -c "%s" -C %s -f %s %s'
268                 cmd = cmd % (python_executable, exec_script_main, d, c, buildtarget)
269                 self.file.write('# PROP %sCmd_Line "%s"\n'
270                                 '# PROP %sRebuild_Opt "-c && %s"\n'
271                                 '# PROP %sTarget_File "%s"\n'
272                                 '# PROP %sBsc_Name ""\n'
273                                 '# PROP %sTarget_Dir ""\n'\
274                                 %(base,cmd,base,cmd,base,buildtarget,base,base))
275
276         self.file.write('\n!ENDIF\n\n'
277                         '# Begin Target\n\n')
278         for kind in confkeys:
279             self.file.write('# Name "%s - Win32 %s"\n' % (name,kind))
280         self.file.write('\n')
281         first = 0
282         for kind in confkeys:
283             if first == 0:
284                 self.file.write('!IF  "$(CFG)" == "%s - Win32 %s"\n\n' % (name,kind))
285                 first = 1
286             else:
287                 self.file.write('!ELSEIF  "$(CFG)" == "%s - Win32 %s"\n\n' % (name,kind))
288         self.file.write('!ENDIF \n\n')
289         self.PrintSourceFiles()
290         self.file.write('# End Target\n'
291                         '# End Project\n')
292
293         # now we pickle some data and add it to the file -- MSDEV will ignore it.
294         pdata = pickle.dumps(self.configs,1)
295         pdata = base64.encodestring(pdata)
296         self.file.write(pdata + '\n')
297         pdata = pickle.dumps(self.sources,1)
298         pdata = base64.encodestring(pdata)
299         self.file.write(pdata + '\n')
300
301     def PrintSourceFiles(self):
302         categories = {' Source Files': 'cpp|c|cxx|l|y|def|odl|idl|hpj|bat',
303                       'Header Files': 'h|hpp|hxx|hm|inl',
304                       'Local Headers': 'h|hpp|hxx|hm|inl',
305                       'Resource Files': 'r|rc|ico|cur|bmp|dlg|rc2|rct|bin|cnt|rtf|gif|jpg|jpeg|jpe',
306                       'Other Files': ''}
307
308         cats = categories.keys()
309         cats.sort()
310         for kind in cats:
311             if not self.sources[kind]:
312                 continue # skip empty groups
313
314             self.file.write('# Begin Group "' + kind + '"\n\n')
315             typelist = categories[kind].replace('|',';')
316             self.file.write('# PROP Default_Filter "' + typelist + '"\n')
317
318             for file in self.sources[kind]:
319                 file = os.path.normpath(file)
320                 self.file.write('# Begin Source File\n\n'
321                                 'SOURCE="' + file + '"\n'
322                                 '# End Source File\n')
323             self.file.write('# End Group\n')
324
325         # add the Conscript file outside of the groups
326         self.file.write('# Begin Source File\n\n'
327                         'SOURCE="' + str(self.source) + '"\n'
328                         '# End Source File\n')
329
330     def Parse(self):
331         try:
332             dspfile = open(self.dspfile,'r')
333         except IOError:
334             return # doesn't exist yet, so can't add anything to configs.
335
336         line = dspfile.readline()
337         while line:
338             if line.find("# End Project") > -1:
339                 break
340             line = dspfile.readline()
341
342         line = dspfile.readline()
343         datas = line
344         while line and line != '\n':
345             line = dspfile.readline()
346             datas = datas + line
347
348         # OK, we've found our little pickled cache of data.
349         try:
350             datas = base64.decodestring(datas)
351             data = pickle.loads(datas)
352         except KeyboardInterrupt:
353             raise
354         except:
355             return # unable to unpickle any data for some reason
356
357         self.configs.update(data)
358
359         data = None
360         line = dspfile.readline()
361         datas = line
362         while line and line != '\n':
363             line = dspfile.readline()
364             datas = datas + line
365
366         # OK, we've found our little pickled cache of data.
367         # it has a "# " in front of it, so we strip that.
368         try:
369             datas = base64.decodestring(datas)
370             data = pickle.loads(datas)
371         except KeyboardInterrupt:
372             raise
373         except:
374             return # unable to unpickle any data for some reason
375
376         self.sources.update(data)
377
378     def Build(self):
379         try:
380             self.file = open(self.dspfile,'w')
381         except IOError, detail:
382             raise SCons.Errors.InternalError, 'Unable to open "' + self.dspfile + '" for writing:' + str(detail)
383         else:
384             self.PrintHeader()
385             self.PrintProject()
386             self.file.close()
387
388 V7DSPHeader = """\
389 <?xml version="1.0" encoding = "Windows-1252"?>
390 <VisualStudioProject
391 \tProjectType="Visual C++"
392 \tVersion="%(versionstr)s"
393 \tName="%(name)s"
394 \tSccProjectName=""
395 \tSccLocalPath=""
396 \tKeyword="MakeFileProj">
397 \t<Platforms>
398 \t\t<Platform
399 \t\t\tName="Win32"/>
400 \t</Platforms>
401 """
402
403 V7DSPConfiguration = """\
404 \t\t<Configuration
405 \t\t\tName="%(capitalized_kind)s|Win32"
406 \t\t\tOutputDirectory="%(outdir)s"
407 \t\t\tIntermediateDirectory="%(outdir)s"
408 \t\t\tConfigurationType="0"
409 \t\t\tUseOfMFC="0"
410 \t\t\tATLMinimizesCRunTimeLibraryUsage="FALSE">
411 \t\t\t<Tool
412 \t\t\t\tName="VCNMakeTool"
413 \t\t\t\tBuildCommandLine="%(cmd)s"
414 \t\t\t\tCleanCommandLine="%(cleancmd)s"
415 \t\t\t\tRebuildCommandLine="%(cmd)s"
416 \t\t\t\tOutput="%(buildtarget)s"/>
417 \t\t</Configuration>
418 """
419
420 class _GenerateV7DSP(_DSPGenerator):
421     """Generates a Project file for MSVS .NET"""
422
423     def __init__(self, dspfile, source, env):
424         _DSPGenerator.__init__(self, dspfile, source, env)
425         self.version = float(env['MSVS_VERSION'])
426         self.versionstr = '7.00'
427         if self.version >= 7.1:
428             self.versionstr = '7.10'
429
430     def PrintHeader(self):
431         self.file.write(V7DSPHeader % self.__dict__)
432
433     def PrintProject(self):
434         self.file.write('\t<Configurations>\n')
435
436         confkeys = self.configs.keys()
437         confkeys.sort()
438         for kind in confkeys:
439             capitalized_kind = kind.capitalize()
440             outdir = self.configs[kind].outdir
441             buildtarget = self.configs[kind].buildtarget
442
443             (d,c) = os.path.split(str(self.conspath))
444             fmt = 'echo Starting SCons &amp;&amp; &quot;%s&quot; -c &quot;%s&quot; -C %s -f %s%s %s'
445             cmd         = fmt % (python_executable, exec_script_main_xml,
446                                  d, c, '', buildtarget)
447             cleancmd    = fmt % (python_executable, exec_script_main_xml,
448                                  d, c, ' -c', buildtarget)
449
450             self.file.write(V7DSPConfiguration % locals())
451
452         self.file.write('\t</Configurations>\n')
453
454         if self.version >= 7.1:
455             self.file.write('\t<References>\n'
456                             '\t</References>\n')
457
458         self.PrintSourceFiles()
459
460         self.file.write('</VisualStudioProject>\n')
461
462         # now we pickle some data and add it to the file -- MSDEV will ignore it.
463         pdata = pickle.dumps(self.configs,1)
464         pdata = base64.encodestring(pdata)
465         self.file.write('<!-- SCons Data:\n' + pdata + '\n')
466         pdata = pickle.dumps(self.sources,1)
467         pdata = base64.encodestring(pdata)
468         self.file.write(pdata + '-->\n')
469
470     def PrintSourceFiles(self):
471         categories = {' Source Files': 'cpp;c;cxx;l;y;def;odl;idl;hpj;bat',
472                       'Header Files': 'h;hpp;hxx;hm;inl',
473                       'Local Headers': 'h;hpp;hxx;hm;inl',
474                       'Resource Files': 'r;rc;ico;cur;bmp;dlg;rc2;rct;bin;cnt;rtf;gif;jpg;jpeg;jpe',
475                       'Other Files': ''}
476
477         self.file.write('\t<Files>\n')
478
479         cats = categories.keys()
480         cats.sort()
481         for kind in cats:
482             if not self.sources[kind]:
483                 continue # skip empty groups
484
485             self.file.write('\t\t<Filter\n'
486                             '\t\t\tName="%s"\n'
487                             '\t\t\tFilter="%s">\n' % (kind, categories[kind]))
488
489             for file in self.sources[kind]:
490                 file = os.path.normpath(file)
491                 self.file.write('\t\t\t<File\n'
492                                 '\t\t\t\tRelativePath="%s">\n'
493                                 '\t\t\t</File>\n' % file)
494
495             self.file.write('\t\t</Filter>\n')
496
497         # add the Conscript file outside of the groups
498         self.file.write('\t\t<File\n'
499                         '\t\t\tRelativePath="%s">\n'
500                         '\t\t</File>\n'
501                         '\t</Files>\n' % str(self.source))
502
503         self.file.write('\t<Globals>\n'
504                         '\t</Globals>\n')
505
506     def Parse(self):
507         try:
508             dspfile = open(self.dspfile,'r')
509         except IOError:
510             return # doesn't exist yet, so can't add anything to configs.
511
512         line = dspfile.readline()
513         while line:
514             if line.find('<!-- SCons Data:') > -1:
515                 break
516             line = dspfile.readline()
517
518         line = dspfile.readline()
519         datas = line
520         while line and line != '\n':
521             line = dspfile.readline()
522             datas = datas + line
523
524         # OK, we've found our little pickled cache of data.
525         try:
526             datas = base64.decodestring(datas)
527             data = pickle.loads(datas)
528         except KeyboardInterrupt:
529             raise
530         except:
531             return # unable to unpickle any data for some reason
532
533         self.configs.update(data)
534
535         data = None
536         line = dspfile.readline()
537         datas = line
538         while line and line != '\n':
539             line = dspfile.readline()
540             datas = datas + line
541
542         # OK, we've found our little pickled cache of data.
543         try:
544             datas = base64.decodestring(datas)
545             data = pickle.loads(datas)
546         except KeyboardInterrupt:
547             raise
548         except:
549             return # unable to unpickle any data for some reason
550
551         self.sources.update(data)
552
553     def Build(self):
554         try:
555             self.file = open(self.dspfile,'w')
556         except IOError, detail:
557             raise SCons.Errors.InternalError, 'Unable to open "' + self.dspfile + '" for writing:' + str(detail)
558         else:
559             self.PrintHeader()
560             self.PrintProject()
561             self.file.close()
562
563 class _DSWGenerator:
564     """ Base class for DSW generators """
565     def __init__(self, dswfile, dspfile, source, env):
566         self.dswfile = os.path.normpath(str(dswfile))
567         self.dspfile = os.path.abspath(str(dspfile))
568         self.env = env
569
570         if self.env.has_key('name'):
571             self.name = self.env['name']
572         else:
573             self.name = os.path.basename(SCons.Util.splitext(self.dspfile)[0])
574
575     def Build(self):
576         pass
577
578 class _GenerateV7DSW(_DSWGenerator):
579     """Generates a Solution file for MSVS .NET"""
580     def __init__(self, dswfile, dspfile, source, env):
581         _DSWGenerator.__init__(self, dswfile, dspfile, source, env)
582
583         self.version = float(self.env['MSVS_VERSION'])
584         self.versionstr = '7.00'
585         if self.version >= 7.1:
586             self.versionstr = '8.00'
587
588         if env.has_key('slnguid') and env['slnguid']:
589             self.slnguid = env['slnguid']
590         else:
591             self.slnguid = _generateGUID(dswfile, self.name)
592
593         self.config = Config()
594         if env.has_key('variant'):
595             self.config.variant = env['variant'].capitalize()
596         else:
597             raise SCons.Errors.InternalError, \
598                   "You must specify a 'variant' argument (i.e. 'Debug' or " +\
599                   "'Release') to create an MSVS Solution File."
600
601         self.configs = {}
602
603         if os.path.exists(self.dswfile):
604             self.Parse()
605
606         self.configs[self.config.variant] = self.config
607
608     def Parse(self):
609         try:
610             dswfile = open(self.dswfile,'r')
611         except IOError:
612             return # doesn't exist yet, so can't add anything to configs.
613
614         line = dswfile.readline()
615         while line:
616             if line[:9] == "EndGlobal":
617                 break
618             line = dswfile.readline()
619
620         line = dswfile.readline()
621         datas = line
622         while line:
623             line = dswfile.readline()
624             datas = datas + line
625
626         # OK, we've found our little pickled cache of data.
627         try:
628             datas = base64.decodestring(datas)
629             data = pickle.loads(datas)
630         except KeyboardInterrupt:
631             raise
632         except:
633             return # unable to unpickle any data for some reason
634
635         self.configs.update(data)
636
637     def PrintSolution(self):
638         """Writes a solution file"""
639         self.file.write('Microsoft Visual Studio Solution File, Format Version %s\n'
640                         # the next line has the GUID for an external makefile project.
641                         'Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "%s", "%s", "%s"\n'
642                         % (self.versionstr, self.name, os.path.basename(self.dspfile), self.slnguid))
643         if self.version >= 7.1:
644             self.file.write('\tProjectSection(ProjectDependencies) = postProject\n'
645                             '\tEndProjectSection\n')
646         self.file.write('EndProject\n'
647                         'Global\n'
648                         '\tGlobalSection(SolutionConfiguration) = preSolution\n')
649         confkeys = self.configs.keys()
650         confkeys.sort()
651         cnt = 0
652         for name in confkeys:
653             self.file.write('\t\tConfigName.%d = %s\n' % (cnt, name.capitalize()))
654             cnt = cnt + 1
655         self.file.write('\tEndGlobalSection\n')
656         if self.version < 7.1:
657             self.file.write('\tGlobalSection(ProjectDependencies) = postSolution\n'
658                             '\tEndGlobalSection\n')
659         self.file.write('\tGlobalSection(ProjectConfiguration) = postSolution\n')
660         for name in confkeys:
661             name = name.capitalize()
662             self.file.write('\t\t%s.%s.ActiveCfg = %s|Win32\n'
663                             '\t\t%s.%s.Build.0 = %s|Win32\n'  %(self.slnguid,name,name,self.slnguid,name,name))
664         self.file.write('\tEndGlobalSection\n'
665                         '\tGlobalSection(ExtensibilityGlobals) = postSolution\n'
666                         '\tEndGlobalSection\n'
667                         '\tGlobalSection(ExtensibilityAddIns) = postSolution\n'
668                         '\tEndGlobalSection\n'
669                         'EndGlobal\n')
670         pdata = pickle.dumps(self.configs,1)
671         pdata = base64.encodestring(pdata)
672         self.file.write(pdata + '\n')
673
674     def Build(self):
675         try:
676             self.file = open(self.dswfile,'w')
677         except IOError, detail:
678             raise SCons.Errors.InternalError, 'Unable to open "' + self.dswfile + '" for writing:' + str(detail)
679         else:
680             self.PrintSolution()
681             self.file.close()
682
683 V6DSWHeader = """\
684 Microsoft Developer Studio Workspace File, Format Version 6.00
685 # WARNING: DO NOT EDIT OR DELETE THIS WORKSPACE FILE!
686
687 ###############################################################################
688
689 Project: "%(name)s"="%(dspfile)s" - Package Owner=<4>
690
691 Package=<5>
692 {{{
693 }}}
694
695 Package=<4>
696 {{{
697 }}}
698
699 ###############################################################################
700
701 Global:
702
703 Package=<5>
704 {{{
705 }}}
706
707 Package=<3>
708 {{{
709 }}}
710
711 ###############################################################################
712 """
713
714 class _GenerateV6DSW(_DSWGenerator):
715     """Generates a Workspace file for MSVS 6.0"""
716
717     def PrintWorkspace(self):
718         """ writes a DSW file """
719         self.file.write(V6DSWHeader % self.__dict__)
720
721     def Build(self):
722         try:
723             self.file = open(self.dswfile,'w')
724         except IOError, detail:
725             raise SCons.Errors.InternalError, 'Unable to open "' + self.dswfile + '" for writing:' + str(detail)
726         else:
727             self.PrintWorkspace()
728             self.file.close()
729
730
731 def GenerateDSP(dspfile, source, env):
732     """Generates a Project file based on the version of MSVS that is being used"""
733
734     if env.has_key('MSVS_VERSION') and float(env['MSVS_VERSION']) >= 7.0:
735         g = _GenerateV7DSP(dspfile, source, env)
736         g.Build()
737     else:
738         g = _GenerateV6DSP(dspfile, source, env)
739         g.Build()
740
741 def GenerateDSW(dswfile, dspfile, source, env):
742     """Generates a Solution/Workspace file based on the version of MSVS that is being used"""
743
744     if env.has_key('MSVS_VERSION') and float(env['MSVS_VERSION']) >= 7.0:
745         g = _GenerateV7DSW(dswfile, dspfile, source, env)
746         g.Build()
747     else:
748         g = _GenerateV6DSW(dswfile, dspfile, source, env)
749         g.Build()
750
751
752 ##############################################################################
753 # Above here are the classes and functions for generation of
754 # DSP/DSW/SLN/VCPROJ files.
755 ##############################################################################
756
757 def get_default_visualstudio_version(env):
758     """Returns the version set in the env, or the latest version
759     installed, if it can find it, or '6.0' if all else fails.  Also
760     updated the environment with what it found."""
761
762     version = '6.0'
763     versions = [version]
764     if not env.has_key('MSVS') or type(env['MSVS']) != types.DictType:
765         env['MSVS'] = {}
766
767     if env.has_key('MSVS_VERSION'):
768         version = env['MSVS_VERSION']
769         versions = [version]
770     else:
771         if SCons.Util.can_read_reg:
772             versions = get_visualstudio_versions()
773             if versions:
774                 version = versions[0] #use highest version by default
775
776     env['MSVS_VERSION'] = version
777     env['MSVS']['VERSIONS'] = versions
778     env['MSVS']['VERSION'] = version
779
780     return version
781
782 def get_visualstudio_versions():
783     """
784     Get list of visualstudio versions from the Windows registry.
785     Returns a list of strings containing version numbers.  An empty list
786     is returned if we were unable to accees the register (for example,
787     we couldn't import the registry-access module) or the appropriate
788     registry keys weren't found.
789     """
790
791     if not SCons.Util.can_read_reg:
792         return []
793
794     HLM = SCons.Util.HKEY_LOCAL_MACHINE
795     K = r'Software\Microsoft\VisualStudio'
796     L = []
797     try:
798         k = SCons.Util.RegOpenKeyEx(HLM, K)
799         i = 0
800         while 1:
801             try:
802                 p = SCons.Util.RegEnumKey(k,i)
803             except SCons.Util.RegError:
804                 break
805             i = i + 1
806             if not p[0] in '123456789' or p in L:
807                 continue
808             # Only add this version number if there is a valid
809             # registry structure (includes the "Setup" key),
810             # and at least some of the correct directories
811             # exist.  Sometimes VS uninstall leaves around
812             # some registry/filesystem turds that we don't
813             # want to trip over.  Also, some valid registry
814             # entries are MSDN entries, not MSVS ('7.1',
815             # notably), and we want to skip those too.
816             try:
817                 SCons.Util.RegOpenKeyEx(HLM, K + '\\' + p + '\\Setup')
818             except SCons.Util.RegError:
819                 continue
820
821             id = []
822             idk = SCons.Util.RegOpenKeyEx(HLM, K + '\\' + p)
823             # This is not always here -- it only exists if the
824             # user installed into a non-standard location (at
825             # least in VS6 it works that way -- VS7 seems to
826             # always write it)
827             try:
828                 id = SCons.Util.RegQueryValueEx(idk, 'InstallDir')
829             except SCons.Util.RegError:
830                 pass
831
832             # If the InstallDir key doesn't exist,
833             # then we check the default locations.
834             if not id or not id[0]:
835                 files_dir = SCons.Platform.win32.get_program_files_dir()
836                 if float(p) < 7.0:
837                     vs = r'Microsoft Visual Studio\Common\MSDev98'
838                 else:
839                     vs = r'Microsoft Visual Studio .NET\Common7\IDE'
840                 id = [ os.path.join(files_dir, vs) ]
841             if os.path.exists(id[0]):
842                 L.append(p)
843     except SCons.Util.RegError:
844         pass
845
846     if not L:
847         return []
848
849     # This is a hack to get around the fact that certain Visual Studio
850     # patches place a "6.1" version in the registry, which does not have
851     # any of the keys we need to find include paths, install directories,
852     # etc.  Therefore we ignore it if it is there, since it throws all
853     # other logic off.
854     try:
855         L.remove("6.1")
856     except ValueError:
857         pass
858
859     L.sort()
860     L.reverse()
861
862     return L
863
864 def is_msvs_installed():
865     """
866     Check the registry for an installed visual studio.
867     """
868     try:
869         v = SCons.Tool.msvs.get_visualstudio_versions()
870         return v
871     except (SCons.Util.RegError, SCons.Errors.InternalError):
872         return 0
873
874 def get_msvs_install_dirs(version = None):
875     """
876     Get installed locations for various msvc-related products, like the .NET SDK
877     and the Platform SDK.
878     """
879
880     if not SCons.Util.can_read_reg:
881         return {}
882
883     if not version:
884         versions = get_visualstudio_versions()
885         if versions:
886             version = versions[0] #use highest version by default
887         else:
888             return {}
889
890     K = 'Software\\Microsoft\\VisualStudio\\' + version
891
892     # vc++ install dir
893     rv = {}
894     try:
895         if (float(version) < 7.0):
896             (rv['VCINSTALLDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE,
897                                                              K + r'\Setup\Microsoft Visual C++\ProductDir')
898         else:
899             (rv['VCINSTALLDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE,
900                                                              K + r'\Setup\VC\ProductDir')
901     except SCons.Util.RegError:
902         pass
903
904     # visual studio install dir
905     if (float(version) < 7.0):
906         try:
907             (rv['VSINSTALLDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE,
908                                                              K + r'\Setup\Microsoft Visual Studio\ProductDir')
909         except SCons.Util.RegError:
910             pass
911
912         if not rv.has_key('VSINSTALLDIR') or not rv['VSINSTALLDIR']:
913             if rv.has_key('VCINSTALLDIR') and rv['VCINSTALLDIR']:
914                 rv['VSINSTALLDIR'] = os.path.dirname(rv['VCINSTALLDIR'])
915             else:
916                 rv['VSINSTALLDIR'] = os.path.join(SCons.Platform.win32.get_program_files_dir(),'Microsoft Visual Studio')
917     else:
918         try:
919             (rv['VSINSTALLDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE,
920                                                              K + r'\Setup\VS\ProductDir')
921         except SCons.Util.RegError:
922             pass
923
924     # .NET framework install dir
925     try:
926         (rv['FRAMEWORKDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE,
927             r'Software\Microsoft\.NETFramework\InstallRoot')
928     except SCons.Util.RegError:
929         pass
930
931     if rv.has_key('FRAMEWORKDIR'):
932         # try and enumerate the installed versions of the .NET framework.
933         contents = os.listdir(rv['FRAMEWORKDIR'])
934         l = re.compile('v[0-9]+.*')
935         versions = []
936         for entry in contents:
937             if l.match(entry):
938                 versions.append(entry)
939
940         def versrt(a,b):
941             # since version numbers aren't really floats...
942             aa = a[1:]
943             bb = b[1:]
944             aal = aa.split('.')
945             bbl = bb.split('.')
946             c = int(bbl[0]) - int(aal[0])
947             if c == 0:
948                 c = int(bbl[1]) - int(aal[1])
949                 if c == 0:
950                     c = int(bbl[2]) - int(aal[2])
951             return c
952
953         versions.sort(versrt)
954
955         rv['FRAMEWORKVERSIONS'] = versions
956         # assume that the highest version is the latest version installed
957         rv['FRAMEWORKVERSION'] = versions[0]
958
959     # .NET framework SDK install dir
960     try:
961         if rv.has_key('FRAMEWORKVERSION') and rv['FRAMEWORKVERSION'][:4] == 'v1.1':
962             key = r'Software\Microsoft\.NETFramework\sdkInstallRootv1.1'
963         else:
964             key = r'Software\Microsoft\.NETFramework\sdkInstallRoot'
965
966         (rv['FRAMEWORKSDKDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE,key)
967
968     except SCons.Util.RegError:
969         pass
970
971     # MS Platform SDK dir
972     try:
973         (rv['PLATFORMSDKDIR'], t) = SCons.Util.RegGetValue(SCons.Util.HKEY_LOCAL_MACHINE,
974             r'Software\Microsoft\MicrosoftSDK\Directories\Install Dir')
975     except SCons.Util.RegError:
976         pass
977
978     if rv.has_key('PLATFORMSDKDIR'):
979         # if we have a platform SDK, try and get some info on it.
980         vers = {}
981         try:
982             loc = r'Software\Microsoft\MicrosoftSDK\InstalledSDKs'
983             k = SCons.Util.RegOpenKeyEx(SCons.Util.HKEY_LOCAL_MACHINE,loc)
984             i = 0
985             while 1:
986                 try:
987                     key = SCons.Util.RegEnumKey(k,i)
988                     sdk = SCons.Util.RegOpenKeyEx(k,key)
989                     j = 0
990                     name = ''
991                     date = ''
992                     version = ''
993                     while 1:
994                         try:
995                             (vk,vv,t) = SCons.Util.RegEnumValue(sdk,j)
996                             if vk.lower() == 'keyword':
997                                 name = vv
998                             if vk.lower() == 'propagation_date':
999                                 date = vv
1000                             if vk.lower() == 'version':
1001                                 version = vv
1002                             j = j + 1
1003                         except SCons.Util.RegError:
1004                             break
1005                     if name:
1006                         vers[name] = (date, version)
1007                     i = i + 1
1008                 except SCons.Util.RegError:
1009                     break
1010             rv['PLATFORMSDK_MODULES'] = vers
1011         except SCons.Util.RegError:
1012             pass
1013
1014     return rv;
1015
1016 def GetMSVSProjectSuffix(target, source, env, for_signature):
1017      return env['MSVS']['PROJECTSUFFIX'];
1018
1019 def GetMSVSSolutionSuffix(target, source, env, for_signature):
1020      return env['MSVS']['SOLUTIONSUFFIX'];
1021
1022 def GenerateProject(target, source, env):
1023     # generate the dsp file, according to the version of MSVS.
1024     builddspfile = target[0]
1025     builddswfile = target[1]
1026     dswfile = builddswfile.srcnode()
1027     dspfile = builddspfile.srcnode()
1028
1029 #     print "SConscript    :",str(source[0])
1030 #     print "DSW file      :",dswfile
1031 #     print "DSP file      :",dspfile
1032 #     print "Build DSW file:",builddswfile
1033 #     print "Build DSP file:",builddspfile
1034
1035     # this detects whether or not we're using a BuildDir
1036     if os.path.abspath(os.path.normcase(str(dspfile))) != \
1037            os.path.abspath(os.path.normcase(str(builddspfile))):
1038         try:
1039             bdsp = open(str(builddspfile), "w+")
1040         except IOError, detail:
1041             print 'Unable to open "' + str(dspfile) + '" for writing:',detail,'\n'
1042             raise
1043
1044         bdsp.write("This is just a placeholder file.\nThe real project file is here:\n%s\n" % dspfile.get_abspath())
1045
1046         try:
1047             bdsw = open(str(builddswfile), "w+")
1048         except IOError, detail:
1049             print 'Unable to open "' + str(dspfile) + '" for writing:',detail,'\n'
1050             raise
1051
1052         bdsw.write("This is just a placeholder file.\nThe real workspace file is here:\n%s\n" % dswfile.get_abspath())
1053
1054     GenerateDSP(dspfile, source, env)
1055     GenerateDSW(dswfile, dspfile, source, env)
1056
1057 def projectEmitter(target, source, env):
1058     """Sets up the DSP and DSW dependencies for an SConscript file."""
1059
1060     if source[0] == target[0]:
1061         source = []
1062
1063     # make sure the suffix is correct for the version of MSVS we're running.
1064     (base, suff) = SCons.Util.splitext(str(target[0]))
1065     suff = env.subst('$MSVSPROJECTSUFFIX')
1066     target[0] = base + suff
1067
1068     dspfile = env.File(target[0]).srcnode()
1069     dswfile = env.File(SCons.Util.splitext(str(dspfile))[0] + env.subst('$MSVSSOLUTIONSUFFIX'))
1070
1071     # XXX Need to find a way to abstract this; the build engine
1072     # shouldn't depend on anything in SCons.Script.
1073     stack = SCons.Script.call_stack
1074     if not source:
1075         source = [stack[-1].sconscript.srcnode()]
1076     source[0].attributes.sconstruct = stack[0].sconscript
1077
1078     bdswpath = SCons.Util.splitext(str(target[0]))[0] + env.subst('$MSVSSOLUTIONSUFFIX')
1079     bdswfile = env.File(bdswpath)
1080
1081     # only make these side effects if they're
1082     # not the same file.
1083     if os.path.abspath(os.path.normcase(str(dspfile))) != \
1084            os.path.abspath(os.path.normcase(str(target[0]))):
1085         env.SideEffect(dspfile, target[0])
1086         env.Precious(dspfile)
1087         # dswfile isn't precious -- it can be blown away and rewritten each time.
1088         env.SideEffect(dswfile, target[0])
1089
1090     return ([target[0],bdswfile], source)
1091
1092 projectGeneratorAction = SCons.Action.Action(GenerateProject, None)
1093
1094 projectBuilder = SCons.Builder.Builder(action = '$MSVSPROJECTCOM',
1095                                        suffix = '$MSVSPROJECTSUFFIX',
1096                                        emitter = projectEmitter)
1097
1098 def generate(env):
1099     """Add Builders and construction variables for Microsoft Visual
1100     Studio project files to an Environment."""
1101     try:
1102         env['BUILDERS']['MSVSProject']
1103     except KeyError:
1104         env['BUILDERS']['MSVSProject'] = projectBuilder
1105
1106     env['MSVSPROJECTCOM'] = projectGeneratorAction
1107
1108     try:
1109         version = get_default_visualstudio_version(env)
1110         # keep a record of some of the MSVS info so the user can use it.
1111         dirs = get_msvs_install_dirs(version)
1112         env['MSVS'].update(dirs)
1113     except (SCons.Util.RegError, SCons.Errors.InternalError):
1114         # we don't care if we can't do this -- if we can't, it's
1115         # because we don't have access to the registry, or because the
1116         # tools aren't installed.  In either case, the user will have to
1117         # find them on their own.
1118         pass
1119
1120     if (float(env['MSVS_VERSION']) < 7.0):
1121         env['MSVS']['PROJECTSUFFIX']  = '.dsp'
1122         env['MSVS']['SOLUTIONSUFFIX'] = '.dsw'
1123     else:
1124         env['MSVS']['PROJECTSUFFIX']  = '.vcproj'
1125         env['MSVS']['SOLUTIONSUFFIX'] = '.sln'
1126
1127     env['GET_MSVSPROJECTSUFFIX']  = GetMSVSProjectSuffix
1128     env['GET_MSVSSOLUTIONSUFFIX']  = GetMSVSSolutionSuffix
1129     env['MSVSPROJECTSUFFIX']  = '${GET_MSVSPROJECTSUFFIX}'
1130     env['MSVSSOLUTIONSUFFIX']  = '${GET_MSVSSOLUTIONSUFFIX}'
1131
1132 def exists(env):
1133     try:
1134         v = SCons.Tool.msvs.get_visualstudio_versions()
1135     except (SCons.Util.RegError, SCons.Errors.InternalError):
1136         pass
1137
1138     if not v:
1139         if env.has_key('MSVS_VERSION') and float(env['MSVS_VERSION']) >= 7.0:
1140             return env.Detect('devenv')
1141         else:
1142             return env.Detect('msdev')
1143     else:
1144         # there's at least one version of MSVS installed.
1145         return 1
1146