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	ext_in = [ '.py', '.c', '.h', '.java', '.pb.cc' ]
73
74	def runnable_status(self):
75		'''
76		self.pars are populated in runnable_status - because this function is being
77		run *before* both self.pars "consumers" - scan() and run()
78
79		set output_dir (node) for the output
80		'''
81
82		for x in self.run_after:
83			if not x.hasrun:
84				return Task.ASK_LATER
85
86		if not getattr(self, 'pars', None):
87			txt = self.inputs[0].read()
88			self.pars = parse_doxy(txt)
89
90			# Override with any parameters passed to the task generator
91			if getattr(self.generator, 'pars', None):
92				for k, v in self.generator.pars.items():
93					self.pars[k] = v
94
95			if self.pars.get('OUTPUT_DIRECTORY'):
96				# Use the path parsed from the Doxyfile as an absolute path
97				output_node = self.inputs[0].parent.get_bld().make_node(self.pars['OUTPUT_DIRECTORY'])
98			else:
99				# If no OUTPUT_PATH was specified in the Doxyfile, build path from the Doxyfile name + '.doxy'
100				output_node = self.inputs[0].parent.get_bld().make_node(self.inputs[0].name + '.doxy')
101			output_node.mkdir()
102			self.pars['OUTPUT_DIRECTORY'] = output_node.abspath()
103
104			self.doxy_inputs = getattr(self, 'doxy_inputs', [])
105			if not self.pars.get('INPUT'):
106				self.doxy_inputs.append(self.inputs[0].parent)
107			else:
108				for i in self.pars.get('INPUT').split():
109					if os.path.isabs(i):
110						node = self.generator.bld.root.find_node(i)
111					else:
112						node = self.inputs[0].parent.find_node(i)
113					if not node:
114						self.generator.bld.fatal('Could not find the doxygen input %r' % i)
115					self.doxy_inputs.append(node)
116
117		if not getattr(self, 'output_dir', None):
118			bld = self.generator.bld
119			# Output path is always an absolute path as it was transformed above.
120			self.output_dir = bld.root.find_dir(self.pars['OUTPUT_DIRECTORY'])
121
122		self.signature()
123		ret = Task.Task.runnable_status(self)
124		if ret == Task.SKIP_ME:
125			# in case the files were removed
126			self.add_install()
127		return ret
128
129	def scan(self):
130		exclude_patterns = self.pars.get('EXCLUDE_PATTERNS','').split()
131		exclude_patterns = [pattern.replace('*/', '**/') for pattern in exclude_patterns]
132		file_patterns = self.pars.get('FILE_PATTERNS','').split()
133		if not file_patterns:
134			file_patterns = DOXY_FILE_PATTERNS.split()
135		if self.pars.get('RECURSIVE') == 'YES':
136			file_patterns = ["**/%s" % pattern for pattern in file_patterns]
137		nodes = []
138		names = []
139		for node in self.doxy_inputs:
140			if os.path.isdir(node.abspath()):
141				for m in node.ant_glob(incl=file_patterns, excl=exclude_patterns):
142					nodes.append(m)
143			else:
144				nodes.append(node)
145		return (nodes, names)
146
147	def run(self):
148		dct = self.pars.copy()
149		code = '\n'.join(['%s = %s' % (x, dct[x]) for x in self.pars])
150		code = code.encode() # for python 3
151		#fmt = DOXY_STR % (self.inputs[0].parent.abspath())
152		cmd = Utils.subst_vars(DOXY_STR, self.env)
153		env = self.env.env or None
154		proc = Utils.subprocess.Popen(cmd, shell=True, stdin=Utils.subprocess.PIPE, env=env, cwd=self.inputs[0].parent.abspath())
155		proc.communicate(code)
156		return proc.returncode
157
158	def post_run(self):
159		nodes = self.output_dir.ant_glob('**/*', quiet=True)
160		for x in nodes:
161			self.generator.bld.node_sigs[x] = self.uid()
162		self.add_install()
163		return Task.Task.post_run(self)
164
165	def add_install(self):
166		nodes = self.output_dir.ant_glob('**/*', quiet=True)
167		self.outputs += nodes
168		if getattr(self.generator, 'install_path', None):
169			if not getattr(self.generator, 'doxy_tar', None):
170				self.generator.add_install_files(install_to=self.generator.install_path,
171					install_from=self.outputs,
172					postpone=False,
173					cwd=self.output_dir,
174					relative_trick=True)
175
176class tar(Task.Task):
177	"quick tar creation"
178	run_str = '${TAR} ${TAROPTS} ${TGT} ${SRC}'
179	color   = 'RED'
180	after   = ['doxygen']
181	def runnable_status(self):
182		for x in getattr(self, 'input_tasks', []):
183			if not x.hasrun:
184				return Task.ASK_LATER
185
186		if not getattr(self, 'tar_done_adding', None):
187			# execute this only once
188			self.tar_done_adding = True
189			for x in getattr(self, 'input_tasks', []):
190				self.set_inputs(x.outputs)
191			if not self.inputs:
192				return Task.SKIP_ME
193		return Task.Task.runnable_status(self)
194
195	def __str__(self):
196		tgt_str = ' '.join([a.path_from(a.ctx.launch_node()) for a in self.outputs])
197		return '%s: %s\n' % (self.__class__.__name__, tgt_str)
198
199@feature('doxygen')
200def process_doxy(self):
201	if not getattr(self, 'doxyfile', None):
202		self.bld.fatal('no doxyfile variable specified??')
203
204	node = self.doxyfile
205	if not isinstance(node, Node.Node):
206		node = self.path.find_resource(node)
207	if not node:
208		self.bld.fatal('doxygen file %s not found' % self.doxyfile)
209
210	# the task instance
211	dsk = self.create_task('doxygen', node, always_run=getattr(self, 'always', False))
212
213	if getattr(self, 'doxy_tar', None):
214		tsk = self.create_task('tar', always_run=getattr(self, 'always', False))
215		tsk.input_tasks = [dsk]
216		tsk.set_outputs(self.path.find_or_declare(self.doxy_tar))
217		if self.doxy_tar.endswith('bz2'):
218			tsk.env['TAROPTS'] = ['cjf']
219		elif self.doxy_tar.endswith('gz'):
220			tsk.env['TAROPTS'] = ['czf']
221		else:
222			tsk.env['TAROPTS'] = ['cf']
223		if getattr(self, 'install_path', None):
224			self.add_install_files(install_to=self.install_path, install_from=tsk.outputs)
225
226def configure(conf):
227	'''
228	Check if doxygen and tar commands are present in the system
229
230	If the commands are present, then conf.env.DOXYGEN and conf.env.TAR
231	variables will be set. Detection can be controlled by setting DOXYGEN and
232	TAR environmental variables.
233	'''
234
235	conf.find_program('doxygen', var='DOXYGEN', mandatory=False)
236	conf.find_program('tar', var='TAR', mandatory=False)
237