3 # Avalanche Studios 2009-2011
7 Redistribution and use in source and binary forms, with or without
8 modification, are permitted provided that the following conditions
11 1. Redistributions of source code must retain the above copyright
12 notice, this list of conditions and the following disclaimer.
14 2. Redistributions in binary form must reproduce the above copyright
15 notice, this list of conditions and the following disclaimer in the
16 documentation and/or other materials provided with the distribution.
18 3. The name of the author may not be used to endorse or promote products
19 derived from this software without specific prior written permission.
21 THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
22 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
25 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
29 STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
30 IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 POSSIBILITY OF SUCH DAMAGE.
35 To add this tool to your project:
39 It can be a good idea to add the sync_exec tool too.
41 To generate solution files:
44 To customize the outputs, provide subclasses in your wscript files:
46 from waflib.extras import msvs
47 class vsnode_target(msvs.vsnode_target):
48 def get_build_command(self, props):
49 # likely to be required
50 return "waf.bat build"
51 def collect_source(self):
52 # likely to be required
54 class msvs_bar(msvs.msvs_generator):
56 msvs.msvs_generator.init(self)
57 self.vsnode_target = vsnode_target
60 * a project can be either a directory or a target, vcxproj files are written only for targets that have source files
61 * each project is a vcxproj file, therefore the project uuid needs only to be a hash of the absolute path
66 import uuid # requires python 2.5
67 from waflib.Build import BuildContext
68 from waflib import Utils, TaskGen, Logs, Task, Context
70 HEADERS_GLOB = '**/(*.h|*.hpp|*.H|*.inl)'
72 PROJECT_TEMPLATE = r'''<?xml version="1.0" encoding="Windows-1252"?>
73 <Project DefaultTargets="Build" ToolsVersion="4.0"
74 xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
76 <ItemGroup Label="ProjectConfigurations">
77 ${for b in project.build_properties}
78 <ProjectConfiguration Include="${b.configuration}|${b.platform}">
79 <Configuration>${b.configuration}</Configuration>
80 <Platform>${b.platform}</Platform>
81 </ProjectConfiguration>
85 <PropertyGroup Label="Globals">
86 <ProjectGuid>{${project.uuid}}</ProjectGuid>
87 <Keyword>MakeFileProj</Keyword>
89 <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
91 ${for b in project.build_properties}
92 <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='${b.configuration}|${b.platform}'" Label="Configuration">
93 <ConfigurationType>Makefile</ConfigurationType>
94 <OutDir>${b.outdir}</OutDir>
98 <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
99 <ImportGroup Label="ExtensionSettings">
102 ${for b in project.build_properties}
103 <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='${b.configuration}|${b.platform}'">
104 <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
108 ${for b in project.build_properties}
109 <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='${b.configuration}|${b.platform}'">
110 <NMakeBuildCommandLine>${xml:project.get_build_command(b)}</NMakeBuildCommandLine>
111 <NMakeReBuildCommandLine>${xml:project.get_rebuild_command(b)}</NMakeReBuildCommandLine>
112 <NMakeCleanCommandLine>${xml:project.get_clean_command(b)}</NMakeCleanCommandLine>
113 <NMakeIncludeSearchPath>${xml:b.includes_search_path}</NMakeIncludeSearchPath>
114 <NMakePreprocessorDefinitions>${xml:b.preprocessor_definitions};$(NMakePreprocessorDefinitions)</NMakePreprocessorDefinitions>
115 <IncludePath></IncludePath>
116 <ExecutablePath>$(ExecutablePath)</ExecutablePath>
118 ${if getattr(b, 'output_file', None)}
119 <NMakeOutput>${xml:b.output_file}</NMakeOutput>
121 ${if getattr(b, 'deploy_dir', None)}
122 <RemoteRoot>${xml:b.deploy_dir}</RemoteRoot>
127 ${for b in project.build_properties}
128 ${if getattr(b, 'deploy_dir', None)}
129 <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='${b.configuration}|${b.platform}'">
131 <DeploymentType>CopyToHardDrive</DeploymentType>
133 </ItemDefinitionGroup>
138 ${for x in project.source}
139 <${project.get_key(x)} Include='${x.abspath()}' />
142 <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
143 <ImportGroup Label="ExtensionTargets">
148 PROJECT_2008_TEMPLATE = r'''<?xml version="1.0" encoding="Windows-1252"?>
150 ProjectType="Visual C++"
152 Name="${xml: project.name}"
153 ProjectGUID="{${project.uuid}}"
155 Keyword="MakeFileProj"
156 TargetFrameworkVersion="196613"
159 ${if project.build_properties}
160 ${for b in project.build_properties}
162 Name="${xml: b.platform}"
166 <!-- This is a default platform, VisualStudioProject must have one -->
175 ${if project.build_properties}
176 ${for b in project.build_properties}
178 Name="${xml: b.configuration}|${xml: b.platform}"
179 OutputDirectory="${xml: b.outdir}"
180 ConfigurationType="0"
181 InheritedPropertySheets="">
184 BuildCommandLine="${xml: project.get_build_command(b)}"
185 ReBuildCommandLine="${xml: project.get_rebuild_command(b)}"
186 CleanCommandLine="${xml: project.get_clean_command(b)}"
187 ${if getattr(b, 'output_file', None)}
188 OutPut="${xml: b.output_file}"
190 PreprocessorDefinitions="${xml: b.preprocessor_definitions}"
191 IncludeSearchPath="${xml: b.includes_search_path}"
193 ForcedUsingAssemblies=""
199 <!-- This is a default configuration, VisualStudioProject must have one -->
209 ${for x in project.source}
211 RelativePath="${x.abspath()}"
212 FileType="${project.get_key(x)}"
217 </VisualStudioProject>
220 FILTER_TEMPLATE = '''<?xml version="1.0" encoding="Windows-1252"?>
221 <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
223 ${for x in project.source}
224 <${project.get_key(x)} Include="${x.abspath()}">
225 <Filter>${x.parent.path_from(project.path)}</Filter>
226 </${project.get_key(x)}>
230 ${for x in project.dirs()}
231 <Filter Include="${x.path_from(project.path)}">
232 <UniqueIdentifier>{${project.make_uuid(x.abspath())}}</UniqueIdentifier>
239 SOLUTION_TEMPLATE = '''Microsoft Visual Studio Solution File, Format Version ${project.numver}
240 # Visual Studio ${project.vsver}
241 ${for p in project.all_projects}
242 Project("{${p.ptype()}}") = "${p.name}", "${p.title}", "{${p.uuid}}"
245 GlobalSection(SolutionConfigurationPlatforms) = preSolution
246 ${if project.all_projects}
247 ${for (configuration, platform) in project.all_projects[0].ctx.project_configurations()}
248 ${configuration}|${platform} = ${configuration}|${platform}
252 GlobalSection(ProjectConfigurationPlatforms) = postSolution
253 ${for p in project.all_projects}
254 ${if hasattr(p, 'source')}
255 ${for b in p.build_properties}
256 {${p.uuid}}.${b.configuration}|${b.platform}.ActiveCfg = ${b.configuration}|${b.platform}
257 ${if getattr(p, 'is_active', None)}
258 {${p.uuid}}.${b.configuration}|${b.platform}.Build.0 = ${b.configuration}|${b.platform}
264 GlobalSection(SolutionProperties) = preSolution
265 HideSolutionNode = FALSE
267 GlobalSection(NestedProjects) = preSolution
268 ${for p in project.all_projects}
270 {${p.uuid}} = {${p.parent.uuid}}
277 COMPILE_TEMPLATE = '''def f(project):
279 def xml_escape(value):
280 return value.replace("&", "&").replace('"', """).replace("'", "'").replace("<", "<").replace(">", ">")
284 #f = open('cmd.txt', 'w')
289 reg_act = re.compile(r"(?P<backslash>\\)|(?P<dollar>\$\$)|(?P<subst>\$\{(?P<code>[^}]*?)\})", re.M)
290 def compile_template(line):
292 Compile a template expression into a python function (like jsps, but way shorter)
297 if g('dollar'): return "$"
299 extr.append(g('code'))
303 line2 = reg_act.sub(repl, line)
304 params = line2.split('<<|@|>>')
314 buf.append(indent * '\t' + txt)
316 for x in range(len(extr)):
318 app("lst.append(%r)" % params[x])
321 if f.startswith('if') or f.startswith('for'):
324 elif f.startswith('py:'):
326 elif f.startswith('endif') or f.startswith('endfor'):
328 elif f.startswith('else') or f.startswith('elif'):
332 elif f.startswith('xml:'):
333 app('lst.append(xml_escape(%s))' % f[4:])
335 #app('lst.append((%s) or "cannot find %s")' % (f, f))
336 app('lst.append(%s)' % f)
340 app("lst.append(%r)" % params[-1])
342 fun = COMPILE_TEMPLATE % "\n\t".join(buf)
344 return Task.funex(fun)
346 re_blank = re.compile('\n\s*\n', re.M)
347 def rm_blank_lines(txt):
348 txt = re_blank.sub('\n', txt)
352 def make_uuid(v, prefix = None):
354 simple utility function
356 if isinstance(v, dict):
357 keys = list(v.keys())
359 tmp = str([(k, v[k]) for k in keys])
362 d = Utils.md5(tmp.encode()).hexdigest().upper()
364 d = '%s%s' % (prefix, d[8:])
365 gid = uuid.UUID(d, version = 4)
366 return str(gid).upper()
368 class build_property(object):
371 class vsnode(object):
373 Abstract class representing visual studio elements
374 We assume that all visual studio nodes have a uuid and a parent
376 def __init__(self, ctx):
377 self.ctx = ctx # msvs context
378 self.name = '' # string, mandatory
379 self.vspath = '' # path in visual studio (name for dirs, absolute path for projects)
380 self.uuid = '' # string, mandatory
381 self.parent = None # parent node for visual studio nesting
385 Override in subclasses...
387 return 'cd /d "%s" & waf.bat' % self.ctx.srcnode.abspath()
391 Return a special uuid for projects written in the solution file
397 Write the project file, by default, do nothing
401 def make_uuid(self, val):
403 Alias for creating uuid values easily (the templates cannot access global variables)
405 return make_uuid(val)
407 class vsnode_vsdir(vsnode):
409 Nodes representing visual studio folders (which do not match the filesystem tree!)
411 VS_GUID_SOLUTIONFOLDER = "2150E333-8FDC-42A3-9474-1A3956D46DE8"
412 def __init__(self, ctx, uuid, name, vspath=''):
413 vsnode.__init__(self, ctx)
414 self.title = self.name = name
416 self.vspath = vspath or name
419 return self.VS_GUID_SOLUTIONFOLDER
421 class vsnode_project(vsnode):
423 Abstract class representing visual studio project elements
424 A project is assumed to be writable, and has a node representing the file to write to
426 VS_GUID_VCPROJ = "8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942"
428 return self.VS_GUID_VCPROJ
430 def __init__(self, ctx, node):
431 vsnode.__init__(self, ctx)
433 self.uuid = make_uuid(node.abspath())
434 self.name = node.name
435 self.title = self.path.abspath()
436 self.source = [] # list of node objects
437 self.build_properties = [] # list of properties (nmake commands, output dir, etc)
441 Get the list of parent folders of the source files (header files included)
442 for writing the filters
445 for x in self.source:
446 if x.parent not in lst:
451 Logs.warn('Creating %r' % self.path)
453 # first write the project file
454 template1 = compile_template(PROJECT_TEMPLATE)
455 proj_str = template1(self)
456 proj_str = rm_blank_lines(proj_str)
457 self.path.write(proj_str)
459 # then write the filter
460 template2 = compile_template(FILTER_TEMPLATE)
461 filter_str = template2(self)
462 filter_str = rm_blank_lines(filter_str)
463 tmp = self.path.parent.make_node(self.path.name + '.filters')
464 tmp.write(filter_str)
466 def get_key(self, node):
468 required for writing the source files
471 if name.endswith('.cpp') or name.endswith('.c'):
475 def collect_properties(self):
477 Returns a list of triplet (configuration, platform, output_directory)
480 for c in self.ctx.configurations:
481 for p in self.ctx.platforms:
488 x.preprocessor_definitions = ''
489 x.includes_search_path = ''
491 # can specify "deploy_dir" too
493 self.build_properties = ret
495 def get_build_params(self, props):
496 opt = '--execsolution=%s' % self.ctx.get_solution_node().abspath()
497 return (self.get_waf(), opt)
499 def get_build_command(self, props):
500 return "%s build %s" % self.get_build_params(props)
502 def get_clean_command(self, props):
503 return "%s clean %s" % self.get_build_params(props)
505 def get_rebuild_command(self, props):
506 return "%s clean build %s" % self.get_build_params(props)
508 class vsnode_alias(vsnode_project):
509 def __init__(self, ctx, node, name):
510 vsnode_project.__init__(self, ctx, node)
512 self.output_file = ''
514 class vsnode_build_all(vsnode_alias):
516 Fake target used to emulate the behaviour of "make all" (starting one process by target is slow)
517 This is the only alias enabled by default
519 def __init__(self, ctx, node, name='build_all_projects'):
520 vsnode_alias.__init__(self, ctx, node, name)
521 self.is_active = True
523 class vsnode_install_all(vsnode_alias):
525 Fake target used to emulate the behaviour of "make install"
527 def __init__(self, ctx, node, name='install_all_projects'):
528 vsnode_alias.__init__(self, ctx, node, name)
530 def get_build_command(self, props):
531 return "%s build install %s" % self.get_build_params(props)
533 def get_clean_command(self, props):
534 return "%s clean %s" % self.get_build_params(props)
536 def get_rebuild_command(self, props):
537 return "%s clean build install %s" % self.get_build_params(props)
539 class vsnode_target(vsnode_project):
541 Visual studio project representing a targets (programs, libraries, etc) and bound
544 def __init__(self, ctx, tg):
546 A project is more or less equivalent to a file/folder
548 base = getattr(ctx, 'projects_dir', None) or tg.path
549 node = base.make_node(tg.name + ctx.project_extension) # the project file as a Node
550 vsnode_project.__init__(self, ctx, node)
551 self.tg = tg # task generator
553 def get_build_params(self, props):
555 Override the default to add the target name
557 opt = '--execsolution=%s' % self.ctx.get_solution_node().abspath()
558 if getattr(self, 'tg', None):
559 opt += " --targets=%s" % self.tg.name
560 return (self.get_waf(), opt)
562 def collect_source(self):
564 source_files = tg.to_nodes(getattr(tg, 'source', []))
565 include_dirs = Utils.to_list(getattr(tg, 'includes', [])) + Utils.to_list(getattr(tg, 'export_includes', []))
567 for x in include_dirs:
568 if isinstance(x, str):
569 x = tg.path.find_node(x)
571 lst = [y for y in x.ant_glob(HEADERS_GLOB, flat=False)]
572 include_files.extend(lst)
575 self.source.extend(list(set(source_files + include_files)))
576 self.source.sort(key=lambda x: x.abspath())
578 def collect_properties(self):
580 Visual studio projects are associated with platforms and configurations (for building especially)
582 super(vsnode_target, self).collect_properties()
583 for x in self.build_properties:
584 x.outdir = self.path.parent.abspath()
585 x.preprocessor_definitions = ''
586 x.includes_search_path = ''
589 tsk = self.tg.link_task
590 except AttributeError:
593 x.output_file = tsk.outputs[0].abspath()
594 x.preprocessor_definitions = ';'.join(tsk.env.DEFINES)
595 x.includes_search_path = ';'.join(self.tg.env.INCPATHS)
597 class msvs_generator(BuildContext):
603 Some data that needs to be present
605 if not getattr(self, 'configurations', None):
606 self.configurations = ['Release'] # LocalRelease, RemoteDebug, etc
607 if not getattr(self, 'platforms', None):
608 self.platforms = ['Win32']
609 if not getattr(self, 'all_projects', None):
610 self.all_projects = []
611 if not getattr(self, 'project_extension', None):
612 self.project_extension = '.vcxproj'
613 if not getattr(self, 'projects_dir', None):
614 self.projects_dir = self.srcnode.make_node('.depproj')
615 self.projects_dir.mkdir()
617 # bind the classes to the object, so that subclass can provide custom generators
618 if not getattr(self, 'vsnode_vsdir', None):
619 self.vsnode_vsdir = vsnode_vsdir
620 if not getattr(self, 'vsnode_target', None):
621 self.vsnode_target = vsnode_target
622 if not getattr(self, 'vsnode_build_all', None):
623 self.vsnode_build_all = vsnode_build_all
624 if not getattr(self, 'vsnode_install_all', None):
625 self.vsnode_install_all = vsnode_install_all
635 if not self.all_envs:
637 self.recurse([self.run_dir])
639 # user initialization
642 # two phases for creating the solution
643 self.collect_projects() # add project objects into "self.all_projects"
644 self.write_files() # write the corresponding project and solution files
646 def collect_projects(self):
648 Fill the list self.all_projects with project objects
649 Fill the list of build targets
651 self.collect_targets()
654 self.all_projects.sort(key=lambda x: getattr(x, 'path', None) and x.path.abspath() or x.name)
656 def write_files(self):
658 Write the project and solution files from the data collected
659 so far. It is unlikely that you will want to change this
661 for p in self.all_projects:
664 # and finally write the solution file
665 node = self.get_solution_node()
667 Logs.warn('Creating %r' % node)
668 template1 = compile_template(SOLUTION_TEMPLATE)
669 sln_str = template1(self)
670 sln_str = rm_blank_lines(sln_str)
673 def get_solution_node(self):
675 The solution filename is required when writing the .vcproj files
676 return self.solution_node and if it does not exist, make one
679 return self.solution_node
683 solution_name = getattr(self, 'solution_name', None)
684 if not solution_name:
685 solution_name = getattr(Context.g_module, Context.APPNAME, 'project') + '.sln'
686 if os.path.isabs(solution_name):
687 self.solution_node = self.root.make_node(solution_name)
689 self.solution_node = self.srcnode.make_node(solution_name)
690 return self.solution_node
692 def project_configurations(self):
694 Helper that returns all the pairs (config,platform)
697 for c in self.configurations:
698 for p in self.platforms:
702 def collect_targets(self):
704 Process the list of task generators
706 for g in self.groups:
708 if not isinstance(tg, TaskGen.task_gen):
712 if not getattr(tg, 'link_task', None):
715 p = self.vsnode_target(self, tg)
716 p.collect_source() # delegate this processing
717 p.collect_properties()
718 self.all_projects.append(p)
720 def add_aliases(self):
722 Add a specific target that emulates the "make all" necessary for Visual studio when pressing F7
723 We also add an alias for "make install" (disabled by default)
725 base = getattr(self, 'projects_dir', None) or tg.path
727 node_project = base.make_node('build_all_projects' + self.project_extension) # Node
728 p_build = self.vsnode_build_all(self, node_project)
729 p_build.collect_properties()
730 self.all_projects.append(p_build)
732 node_project = base.make_node('install_all_projects' + self.project_extension) # Node
733 p_install = self.vsnode_install_all(self, node_project)
734 p_install.collect_properties()
735 self.all_projects.append(p_install)
737 n = self.vsnode_vsdir(self, make_uuid(self.srcnode.abspath() + 'build_aliases'), "build_aliases")
738 p_build.parent = p_install.parent = n
739 self.all_projects.append(n)
741 def collect_dirs(self):
743 Create the folder structure in the Visual studio project view
746 def make_parents(proj):
747 # look at a project, try to make a parent
748 if proj.iter_path in seen:
750 seen.add(proj.iter_path)
752 # create a project representing the folder "x"
755 n = self.vsnode_vsdir(self, make_uuid(x.abspath()), x.name)
756 n.iter_path = x.parent
758 self.all_projects.append(n)
760 # recurse up to the project directory
761 if x.height() > self.srcnode.height() + 1:
764 for p in self.all_projects[:]: # iterate over a copy of all projects
765 if not getattr(p, 'tg', None):
766 # but only projects that have a task generator
769 # make a folder for each task generator
770 p.iter_path = p.tg.path
775 def __init__(self, *k, **kw):
776 cls.__init__(self, *k, **kw)
777 self.project_template = PROJECT_2008_TEMPLATE
778 def get_key(self, node):
780 required for writing the source files.
781 If empty, visual studio uses the file extension,
783 0: C/C++ Code, 1: C++ Class, 2: C++ Header File, 3: C++ Form,
784 4: C++ Control, 5: Text File, 6: DEF File, 7: IDL File,
785 8: Makefile, 9: RGS File, 10: RC File, 11: RES File, 12: XSD File,
786 13: XML File, 14: HTML File, 15: CSS File, 16: Bitmap, 17: Icon,
787 18: Resx File, 19: BSC File, 20: XSX File, 21: C++ Web Service,
788 22: ASAX File, 23: Asp Page, 24: Document, 25: Discovery File,
789 26: C# File, 27: eFileTypeClassDiagram, 28: MHTML Document,
790 29: Property Sheet, 30: Cursor, 31: Manifest, 32: eFileTypeRDLC
794 Logs.warn('Creating %r' % self.path)
795 # write the project file only (no filters in vs2008)
796 template1 = compile_template(self.project_template)
797 proj_str = template1(self)
798 proj_str = rm_blank_lines(proj_str)
799 self.path.write(proj_str)
802 class msvs_2008_generator(msvs_generator):
804 fun = msvs_generator.fun
805 __doc__ = msvs_generator.__doc__
808 if not getattr(self, 'project_extension', None):
809 self.project_extension = '_2008.vcproj'
810 if not getattr(self, 'solution_name', None):
811 self.solution_name = getattr(Context.g_module, Context.APPNAME, 'project') + '_2008.sln'
813 if not getattr(self, 'vsnode_target', None):
814 self.vsnode_target = wrap_2008(vsnode_target)
815 if not getattr(self, 'vsnode_build_all', None):
816 self.vsnode_build_all = wrap_2008(vsnode_build_all)
817 if not getattr(self, 'vsnode_install_all', None):
818 self.vsnode_install_all = wrap_2008(vsnode_install_all)
820 msvs_generator.init(self)
826 If the msvs option is used, try to detect if the build is made from visual studio
828 ctx.add_option('--execsolution', action='store', help='when building with visual studio, use a build state file')
830 old = BuildContext.execute
831 def override_build_state(ctx):
833 uns = ctx.options.execsolution.replace('.sln', rm)
834 uns = ctx.root.make_node(uns)
840 uns = ctx.options.execsolution.replace('.sln', add)
841 uns = ctx.root.make_node(uns)
847 if ctx.options.execsolution:
848 ctx.launch_dir = Context.top_dir # force a build for the whole project (invalid cwd when called by visual studio)
849 lock('.lastbuildstate', '.unsuccessfulbuild')
851 lock('.unsuccessfulbuild', '.lastbuildstate')
854 BuildContext.execute = override_build_state