1#! /usr/bin/env python
2# encoding: UTF-8
3# Thomas Nagy 2008-2010 (ita)
4
5"""
6
7Doxygen support
8
9Variables passed to bld():
10* doxyfile -- the Doxyfile to use
11* doxy_tar -- destination archive for generated documentation (if desired)
12* install_path -- where to install the documentation
13* pars -- dictionary overriding doxygen configuration settings
14
15When using this tool, the wscript will look like:
16
17	def options(opt):
18		opt.load('doxygen')
19
20	def configure(conf):
21		conf.load('doxygen')
22		# check conf.env.DOXYGEN, if it is mandatory
23
24	def build(bld):
25		if bld.env.DOXYGEN:
26			bld(features="doxygen", doxyfile='Doxyfile', ...)
27"""
28
29import os, os.path, re
30from collections import OrderedDict
31from waflib import Task, Utils, Node
32from waflib.TaskGen import feature
33
34DOXY_STR = '"${DOXYGEN}" - '
35DOXY_FMTS = 'html latex man rft xml'.split()
36DOXY_FILE_PATTERNS = '*.' + ' *.'.join('''
37c cc cxx cpp c++ java ii ixx ipp i++ inl h hh hxx hpp h++ idl odl cs php php3
38inc m mm py f90c cc cxx cpp c++ java ii ixx ipp i++ inl h hh hxx
39'''.split())
40
41re_rl = re.compile('\\\\\r*\n', re.MULTILINE)
42re_nl = re.compile('\r*\n', re.M)
43def parse_doxy(txt):
44	'''
45	Parses a doxygen file.
46	Returns an ordered dictionary. We cannot return a default dictionary, as the
47	order in which the entries are reported does matter, especially for the
48	'@INCLUDE' lines.
49	'''
50	tbl = OrderedDict()
51	txt   = re_rl.sub('', txt)
52	lines = re_nl.split(txt)
53	for x in lines:
54		x = x.strip()
55		if not x or x.startswith('#') or x.find('=') < 0:
56			continue
57		if x.find('+=') >= 0:
58			tmp = x.split('+=')
59			key = tmp[0].strip()
60			if key in tbl:
61				tbl[key] += ' ' + '+='.join(tmp[1:]).strip()
62			else:
63				tbl[key] = '+='.join(tmp[1:]).strip()
64		else:
65			tmp = x.split('=')
66			tbl[tmp[0].strip()] = '='.join(tmp[1:]).strip()
67	return tbl
68
69class doxygen(Task.Task):
70	vars  = ['DOXYGEN', 'DOXYFLAGS']
71	color = 'BLUE'
72
73	def runnable_status(self):
74		'''
75		self.pars are populated in runnable_status - because this function is being
76		run *before* both self.pars "consumers" - scan() and run()
77
78		set output_dir (node) for the output
79		'''
80
81		for x in self.run_after:
82			if not x.hasrun:
83				return Task.ASK_LATER
84
85		if not getattr(self, 'pars', None):
86			txt = self.inputs[0].read()
87			self.pars = parse_doxy(txt)
88
89			# Override with any parameters passed to the task generator
90			if getattr(self.generator, 'pars', None):
91				for k, v in self.generator.pars.items():
92					self.pars[k] = v
93
94			if self.pars.get('OUTPUT_DIRECTORY'):
95				# Use the path parsed from the Doxyfile as an absolute path
96				output_node = self.inputs[0].parent.get_bld().make_node(self.pars['OUTPUT_DIRECTORY'])
97			else:
98				# If no OUTPUT_PATH was specified in the Doxyfile, build path from the Doxyfile name + '.doxy'
99				output_node = self.inputs[0].parent.get_bld().make_node(self.inputs[0].name + '.doxy')
100			output_node.mkdir()
101			self.pars['OUTPUT_DIRECTORY'] = output_node.abspath()
102
103			self.doxy_inputs = getattr(self, 'doxy_inputs', [])
104			if not self.pars.get('INPUT'):
105				self.doxy_inputs.append(self.inputs[0].parent)
106			else:
107				for i in self.pars.get('INPUT').split():
108					if os.path.isabs(i):
109						node = self.generator.bld.root.find_node(i)
110					else:
111						node = self.inputs[0].parent.find_node(i)
112					if not node:
113						self.generator.bld.fatal('Could not find the doxygen input %r' % i)
114					self.doxy_inputs.append(node)
115
116		if not getattr(self, 'output_dir', None):
117			bld = self.generator.bld
118			# Output path is always an absolute path as it was transformed above.
119			self.output_dir = bld.root.find_dir(self.pars['OUTPUT_DIRECTORY'])
120
121		self.signature()
122		ret = Task.Task.runnable_status(self)
123		if ret == Task.SKIP_ME:
124			# in case the files were removed
125			self.add_install()
126		return ret
127
128	def scan(self):
129		exclude_patterns = self.pars.get('EXCLUDE_PATTERNS','').split()
130		exclude_patterns = [pattern.replace('*/', '**/') for pattern in exclude_patterns]
131		file_patterns = self.pars.get('FILE_PATTERNS','').split()
132		if not file_patterns:
133			file_patterns = DOXY_FILE_PATTERNS.split()
134		if self.pars.get('RECURSIVE') == 'YES':
135			file_patterns = ["**/%s" % pattern for pattern in file_patterns]
136		nodes = []
137		names = []
138		for node in self.doxy_inputs:
139			if os.path.isdir(node.abspath()):
140				for m in node.ant_glob(incl=file_patterns, excl=exclude_patterns):
141					nodes.append(m)
142			else:
143				nodes.append(node)
144		return (nodes, names)
145
146	def run(self):
147		dct = self.pars.copy()
148		code = '\n'.join(['%s = %s' % (x, dct[x]) for x in self.pars])
149		code = code.encode() # for python 3
150		#fmt = DOXY_STR % (self.inputs[0].parent.abspath())
151		cmd = Utils.subst_vars(DOXY_STR, self.env)
152		env = self.env.env or None
153		proc = Utils.subprocess.Popen(cmd, shell=True, stdin=Utils.subprocess.PIPE, env=env, cwd=self.inputs[0].parent.abspath())
154		proc.communicate(code)
155		return proc.returncode
156
157	def post_run(self):
158		nodes = self.output_dir.ant_glob('**/*', quiet=True)
159		for x in nodes:
160			self.generator.bld.node_sigs[x] = self.uid()
161		self.add_install()
162		return Task.Task.post_run(self)
163
164	def add_install(self):
165		nodes = self.output_dir.ant_glob('**/*', quiet=True)
166		self.outputs += nodes
167		if getattr(self.generator, 'install_path', None):
168			if not getattr(self.generator, 'doxy_tar', None):
169				self.generator.add_install_files(install_to=self.generator.install_path,
170					install_from=self.outputs,
171					postpone=False,
172					cwd=self.output_dir,
173					relative_trick=True)
174
175class tar(Task.Task):
176	"quick tar creation"
177	run_str = '${TAR} ${TAROPTS} ${TGT} ${SRC}'
178	color   = 'RED'
179	after   = ['doxygen']
180	def runnable_status(self):
181		for x in getattr(self, 'input_tasks', []):
182			if not x.hasrun:
183				return Task.ASK_LATER
184
185		if not getattr(self, 'tar_done_adding', None):
186			# execute this only once
187			self.tar_done_adding = True
188			for x in getattr(self, 'input_tasks', []):
189				self.set_inputs(x.outputs)
190			if not self.inputs:
191				return Task.SKIP_ME
192		return Task.Task.runnable_status(self)
193
194	def __str__(self):
195		tgt_str = ' '.join([a.path_from(a.ctx.launch_node()) for a in self.outputs])
196		return '%s: %s\n' % (self.__class__.__name__, tgt_str)
197
198@feature('doxygen')
199def process_doxy(self):
200	if not getattr(self, 'doxyfile', None):
201		self.bld.fatal('no doxyfile variable specified??')
202
203	node = self.doxyfile
204	if not isinstance(node, Node.Node):
205		node = self.path.find_resource(node)
206	if not node:
207		self.bld.fatal('doxygen file %s not found' % self.doxyfile)
208
209	# the task instance
210	dsk = self.create_task('doxygen', node)
211
212	if getattr(self, 'doxy_tar', None):
213		tsk = self.create_task('tar')
214		tsk.input_tasks = [dsk]
215		tsk.set_outputs(self.path.find_or_declare(self.doxy_tar))
216		if self.doxy_tar.endswith('bz2'):
217			tsk.env['TAROPTS'] = ['cjf']
218		elif self.doxy_tar.endswith('gz'):
219			tsk.env['TAROPTS'] = ['czf']
220		else:
221			tsk.env['TAROPTS'] = ['cf']
222		if getattr(self, 'install_path', None):
223			self.add_install_files(install_to=self.install_path, install_from=tsk.outputs)
224
225def configure(conf):
226	'''
227	Check if doxygen and tar commands are present in the system
228
229	If the commands are present, then conf.env.DOXYGEN and conf.env.TAR
230	variables will be set. Detection can be controlled by setting DOXYGEN and
231	TAR environmental variables.
232	'''
233
234	conf.find_program('doxygen', var='DOXYGEN', mandatory=False)
235	conf.find_program('tar', var='TAR', mandatory=False)
236