1#! /usr/bin/env python
2# encoding: utf-8
3# Alexander Afanasyev (UCLA), 2014
4
5"""
6Enable precompiled C++ header support (currently only clang++ and g++ are supported)
7
8To use this tool, wscript should look like:
9
10	def options(opt):
11		opt.load('pch')
12		# This will add `--with-pch` configure option.
13		# Unless --with-pch during configure stage specified, the precompiled header support is disabled
14
15	def configure(conf):
16		conf.load('pch')
17		# this will set conf.env.WITH_PCH if --with-pch is specified and the supported compiler is used
18		# Unless conf.env.WITH_PCH is set, the precompiled header support is disabled
19
20	def build(bld):
21		bld(features='cxx pch',
22			target='precompiled-headers',
23			name='precompiled-headers',
24			headers='a.h b.h c.h', # headers to pre-compile into `precompiled-headers`
25
26			# Other parameters to compile precompiled headers
27			# includes=...,
28			# export_includes=...,
29			# use=...,
30			# ...
31
32			# Exported parameters will be propagated even if precompiled headers are disabled
33		)
34
35		bld(
36			target='test',
37			features='cxx cxxprogram',
38			source='a.cpp b.cpp d.cpp main.cpp',
39			use='precompiled-headers',
40		)
41
42		# or
43
44		bld(
45			target='test',
46			features='pch cxx cxxprogram',
47			source='a.cpp b.cpp d.cpp main.cpp',
48			headers='a.h b.h c.h',
49		)
50
51Note that precompiled header must have multiple inclusion guards.  If the guards are missing, any benefit of precompiled header will be voided and compilation may fail in some cases.
52"""
53
54import os
55from waflib import Task, TaskGen, Utils
56from waflib.Tools import c_preproc, cxx
57
58
59PCH_COMPILER_OPTIONS = {
60	'clang++': [['-include'], '.pch', ['-x', 'c++-header']],
61	'g++':     [['-include'], '.gch', ['-x', 'c++-header']],
62}
63
64
65def options(opt):
66	opt.add_option('--without-pch', action='store_false', default=True, dest='with_pch', help='''Try to use precompiled header to speed up compilation (only g++ and clang++)''')
67
68def configure(conf):
69	if (conf.options.with_pch and conf.env['COMPILER_CXX'] in PCH_COMPILER_OPTIONS.keys()):
70		conf.env.WITH_PCH = True
71		flags = PCH_COMPILER_OPTIONS[conf.env['COMPILER_CXX']]
72		conf.env.CXXPCH_F = flags[0]
73		conf.env.CXXPCH_EXT = flags[1]
74		conf.env.CXXPCH_FLAGS = flags[2]
75
76
77@TaskGen.feature('pch')
78@TaskGen.before('process_source')
79def apply_pch(self):
80	if not self.env.WITH_PCH:
81		return
82
83	if getattr(self.bld, 'pch_tasks', None) is None:
84		self.bld.pch_tasks = {}
85
86	if getattr(self, 'headers', None) is None:
87		return
88
89	self.headers = self.to_nodes(self.headers)
90
91	if getattr(self, 'name', None):
92		try:
93			task = self.bld.pch_tasks["%s.%s" % (self.name, self.idx)]
94			self.bld.fatal("Duplicated 'pch' task with name %r" % "%s.%s" % (self.name, self.idx))
95		except KeyError:
96			pass
97
98	out = '%s.%d%s' % (self.target, self.idx, self.env['CXXPCH_EXT'])
99	out = self.path.find_or_declare(out)
100	task = self.create_task('gchx', self.headers, out)
101
102	# target should be an absolute path of `out`, but without precompiled header extension
103	task.target = out.abspath()[:-len(out.suffix())]
104
105	self.pch_task = task
106	if getattr(self, 'name', None):
107		self.bld.pch_tasks["%s.%s" % (self.name, self.idx)] = task
108
109@TaskGen.feature('cxx')
110@TaskGen.after_method('process_source', 'propagate_uselib_vars')
111def add_pch(self):
112	if not (self.env['WITH_PCH'] and getattr(self, 'use', None) and getattr(self, 'compiled_tasks', None) and getattr(self.bld, 'pch_tasks', None)):
113		return
114
115	pch = None
116	# find pch task, if any
117
118	if getattr(self, 'pch_task', None):
119		pch = self.pch_task
120	else:
121		for use in Utils.to_list(self.use):
122			try:
123				pch = self.bld.pch_tasks[use]
124			except KeyError:
125				pass
126
127	if pch:
128		for x in self.compiled_tasks:
129			x.env.append_value('CXXFLAGS', self.env['CXXPCH_F'] + [pch.target])
130
131class gchx(Task.Task):
132	run_str = '${CXX} ${ARCH_ST:ARCH} ${CXXFLAGS} ${CXXPCH_FLAGS} ${FRAMEWORKPATH_ST:FRAMEWORKPATH} ${CPPPATH_ST:INCPATHS} ${DEFINES_ST:DEFINES} ${CXXPCH_F:SRC} ${CXX_SRC_F}${SRC[0].abspath()} ${CXX_TGT_F}${TGT[0].abspath()} ${CPPFLAGS}'
133	scan    = c_preproc.scan
134	color   = 'BLUE'
135	ext_out=['.h']
136
137	def runnable_status(self):
138		try:
139			node_deps = self.generator.bld.node_deps[self.uid()]
140		except KeyError:
141			node_deps = []
142		ret = Task.Task.runnable_status(self)
143		if ret == Task.SKIP_ME and self.env.CXX_NAME == 'clang':
144			t = os.stat(self.outputs[0].abspath()).st_mtime
145			for n in self.inputs + node_deps:
146				if os.stat(n.abspath()).st_mtime > t:
147					return Task.RUN_ME
148		return ret
149