1 # The Doxygen builder is based on the Doxygen builder from:
3 # Astxx, the Asterisk C++ API and Utility Library.
4 # Copyright (C) 2005, 2006 Matthew A. Nicholson
5 # Copyright (C) 2006 Tim Blechmann
7 # This library is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU Lesser General Public
9 # License version 2.1 as published by the Free Software Foundation.
11 # This library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # Lesser General Public License for more details.
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with this library; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 # The Modifications are Copyright (C) 2006,2007,2008,2009
21 # Fraunhofer Institute for Open Communication Systems (FOKUS)
22 # Competence Center NETwork research (NET), St. Augustin, GERMANY
23 # Stefan Bund <g0dil@berlios.de>
25 # I (g0dil@berlios.de) have been fighting 4 problems in this
27 # - A Directory target will *not* call any source scanners
28 # - A Directory target will interpret the directory contents as
29 # sources not targets. This means, that if a command creates that
30 # directory plus contents, the target will never be up-to-date
31 # (since the directory contents will change with every call of
33 # - Theres a bug in SCons which will produce an error message for
34 # directory targets if dir.sources is not set explicitly
35 # - the first argument to env.Clean() must be the command line target,
36 # with which scons was invoked. This does not help to add aditional
37 # files or directories to be cleaned if you don't know that target
38 # (it's not possible to say 'if you clean this file, also clean that
39 # one' which is, what I had expected env.Clean to do).
41 # Together, these problems have produced several difficulties. I have
43 # - Adding an (empty) stamp file as a (file) target. This target will
44 # cause source scanners to be invoked
45 # - Adding the documentation directory as a target (so it will be
46 # cleaned up which env.Clean doesn't help me to do), but *only* if
47 # scons is called with the -c option
48 # - Setting dir.sources to the known source-list to silence the error
49 # message whenever a directory is added as a target
51 # You will find all this in the DoxyEmitter
53 import os, sys, traceback, string
57 from fnmatch import fnmatch
61 def __init__(self,stream):
68 VARIABLE_RE = re.compile("[@A-Z_]+")
69 OPERATOR_RE = re.compile("\\+?=")
70 VALUE_RE = re.compile("\\S+")
76 self._buffer = self._stream.readline()
80 self._buffer = self._buffer.strip()
83 def _skip(self, nchars=0):
84 self._buffer = self._buffer[nchars:].strip()
85 while self._buffer[:1] == '\\' and not self.eof():
87 if self._buffer[:1] == '#':
90 def _fillbuffer(self):
91 while not self._buffer and not self.eof():
95 def _token(self, re, read=False):
96 if not self._buffer and read:
100 m = re.match(self._buffer)
102 v = self._buffer[:m.end()]
106 raise ValueError,"Invalid input"
108 def var(self): return self._token(self.VARIABLE_RE, True)
109 def op(self): return self._token(self.OPERATOR_RE)
114 if self._buffer[0] == '"':
116 m = self.VALUE_RE.match(self._buffer)
118 v = self._buffer[:m.end()]
127 QSKIP_RE = re.compile("[^\\\"]+")
130 self._buffer = self._buffer[1:]
133 m = self.QSKIP_RE.match(self._buffer)
135 v += self._buffer[:m.end()]
136 self._buffer = self._buffer[m.end():]
137 if self._buffer[:1] == '"':
140 if self._buffer[:1] == '\\' and len(self._buffer)>1:
142 self._buffer = self._buffer[2:]
144 raise ValueError,"Unexpected charachter in string"
145 raise ValueError,"Unterminated string"
150 class DoxyfileParser:
152 ENVVAR_RE = re.compile(r"\$\(([0-9A-Za-z_-]+)\)")
154 def __init__(self, node, env, include_path=None, items = None):
157 self._include_path = include_path or []
158 self._lexer = DoxyfileLexer(file(node.srcnode().get_path()))
160 self._items = items or {}
164 var = self._lexer.var()
166 op = self._lexer.op()
167 value = [ self._envsub(v) for v in self._lexer ]
169 raise ValueError,"Missing value in assignment"
171 self._meta(var,op,value)
173 self._items[var] = value
175 self._items.setdefault(var,[]).extend(value)
177 def _envsub(self,value):
178 return self.ENVVAR_RE.sub(lambda m, env=self._env : str(env.get(m.group(1),"")), value)
180 def _meta(self, cmd, op, value):
184 except AttributeError:
185 raise ValueError,'Unknown meta command ' + cmd
188 def _INCLUDE(self, op, value):
190 raise ValueError,"Invalid argument to @INCLUDE"
192 for d in [ self._dir.get_path() ] + self._include_path:
193 p = os.path.join(d,value[0])
194 if os.path.exists(p):
195 self._items.setdefault('@INCLUDE',[]).append(p)
196 parser = DoxyfileParser(self._node.dir.File(p), self._env, self._include_path, self._items)
200 raise ValueError,"@INCLUDE file '%s' not found" % value[0]
202 def _INCLUDE_PATH(self, op, value):
203 self._include_path.extend(value)
208 def DoxyfileParse(env,node):
209 # We don't parse source files which do not contain the word 'doxyfile'. SCons will
210 # pass other dependencies to DoxyfileParse which are not doxyfiles ... grmpf ...
212 ENV.update(env.get("ENV",{}))
213 ENV.update(env.get("DOXYENV", {}))
214 parser = DoxyfileParser(node,ENV)
217 except ValueError, v:
218 print "WARNING: Error while parsing doxygen configuration '%s': %s" % (str(file),str(v))
220 data = parser.items()
221 for k,v in data.items():
222 if not v : del data[k]
223 elif k in ("LAYOUT_FILE", "INPUT", "FILE_PATTERNS", "EXCLUDE_PATTERNS", "@INCLUDE", "TAGFILES") : continue
224 elif len(v)==1 : data[k] = v[0]
227 def DoxySourceScan(node, env, path):
229 Doxygen Doxyfile source scanner. This should scan the Doxygen file and add
230 any files used to generate docs to the list of source files.
233 ('HTML', 'LAYOUT_FILE'),
235 ('HTML', 'HTML_HEADER'),
236 ('HTML', 'HTML_FOOTER'),
238 (None, 'INPUT_FILTER'),
242 "HTML" : ("YES", "html"),
243 "LATEX" : ("YES", "latex"),
244 "RTF" : ("NO", "rtf"),
245 "MAN" : ("YES", "man"),
246 "XML" : ("NO", "xml"),
249 default_file_patterns = (
250 '*.c', '*.cc', '*.cxx', '*.cpp', '*.c++', '*.java', '*.ii', '*.ixx',
251 '*.ipp', '*.i++', '*.inl', '*.h', '*.hh ', '*.hxx', '*.hpp', '*.h++',
252 '*.idl', '*.odl', '*.cs', '*.php', '*.php3', '*.inc', '*.m', '*.mm',
256 default_exclude_patterns = (
261 basedir = node.dir.abspath
262 data = DoxyfileParse(env, node)
263 recursive = data.get("RECURSIVE", "NO").upper()=="YES"
264 file_patterns = data.get("FILE_PATTERNS", default_file_patterns)
265 exclude_patterns = data.get("EXCLUDE_PATTERNS", default_exclude_patterns)
267 for i in data.get("INPUT", [ "." ]):
268 input = os.path.normpath(os.path.join(basedir,i))
269 if os.path.isfile(input):
270 sources.append(input)
271 elif os.path.isdir(input):
272 if recursive : entries = os.walk(input)
273 else : entries = [ (input, [], os.listdir(input)) ]
274 for root, dirs, files in entries:
276 filename = os.path.normpath(os.path.join(root, f))
277 if ( reduce(lambda x, y: x or fnmatch(f, y), file_patterns, False)
278 and not reduce(lambda x, y: x or fnmatch(f, y), exclude_patterns, False) ):
279 sources.append(filename)
281 for fmt, key in dep_add_keys:
282 if data.has_key(key) and \
283 (fmt is None or data.get("GENERATE_%s" % fmt, output_formats[fmt][0]).upper() == "YES"):
284 elt = env.Flatten(env.subst_list(data[key]))
285 sources.extend([ os.path.normpath(os.path.join(basedir,f))
288 sources = map( lambda path: env.File(path), sources )
291 def DoxySourceScanCheck(node, env):
292 """Check if we should scan this file"""
293 return os.path.isfile(node.path) and 'doxyfile' in node.name.lower()
295 def DoxyEmitter(source, target, env):
296 """Doxygen Doxyfile emitter"""
297 # possible output formats and their default values and output locations
299 "HTML" : ("YES", "html"),
300 "LATEX" : ("YES", "latex"),
301 "RTF" : ("NO", "rtf"),
302 "MAN" : ("YES", "man"),
303 "XML" : ("NO", "xml"),
306 data = DoxyfileParse(env, source[0])
309 if data.get("OUTPUT_DIRECTORY",""):
310 out_dir = data["OUTPUT_DIRECTORY"]
311 dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir) )
313 if env.GetOption('clean'):
315 return (targets, source)
319 # add our output locations
321 for (k, v) in output_formats.iteritems():
322 if data.get("GENERATE_" + k, v[0]).upper() == "YES" and data.get(k + "_OUTPUT", v[1]):
323 dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir, data.get(k + "_OUTPUT", v[1])) )
324 if k == "HTML" : html_dir = dir
326 node = env.File( os.path.join(dir.abspath, k.lower()+".stamp" ) )
328 if env.GetOption('clean'): targets.append(dir)
330 if data.get("GENERATE_TAGFILE",""):
331 targets.append(env.File( os.path.join(source[0].dir.abspath, data["GENERATE_TAGFILE"]) ))
333 if data.get("SEARCHENGINE","NO").upper() == "YES" and html_dir:
334 targets.append(env.File( os.path.join(html_dir.abspath, "search.idx") ))
336 # don't clobber targets
340 return (targets, source)
342 def doxyNodeHtmlDir(env,node):
343 if not node.sources : return None
344 data = DoxyfileParse(env, node.sources[0])
345 if data.get("GENERATE_HTML",'YES').upper() != 'YES' : return None
346 return os.path.normpath(os.path.join( node.sources[0].dir.abspath,
347 data.get("OUTPUT_DIRECTORY","."),
348 data.get("HTML_OUTPUT","html") ))
350 def relpath(source, target):
351 source = os.path.normpath(source)
352 target = os.path.normpath(target)
353 prefix = os.path.dirname(os.path.commonprefix((source,target)))
354 prefix_len = prefix and len(prefix.split(os.sep)) or 0
355 source_elts = source.split(os.sep)
356 target_elts = target.split(os.sep)
357 if source_elts[0] == '..' or target_elts[0] == '..':
358 raise ValueError, "invalid relapth args"
359 return os.path.join(*([".."] * (len(source_elts) - prefix_len) +
360 target_elts[prefix_len:]))
362 def doxyAction(target, source, env):
365 for k,v in env.get('DOXYENV',[]).iteritems() : e[k] = env.subst(v)
366 SCons.Action.Action("$DOXYGENCOM")(target, source, env.Clone(ENV = e), show=False)
368 def doxyActionStr(target, source, env):
369 return env.subst("$DOXYGENCOM",target=target,source=source)
373 Add builders and construction variables for the
374 Doxygen tool. This is currently for Doxygen 1.4.6.
376 doxyfile_scanner = env.Scanner(
379 scan_check = DoxySourceScanCheck,
382 doxyfile_builder = env.Builder(
383 action = [ SCons.Action.Action(doxyAction, doxyActionStr),
384 SCons.Action.Action([ "touch $TARGETS" ]) ],
385 emitter = DoxyEmitter,
386 target_factory = env.fs.Entry,
387 single_source = True,
388 source_scanner = doxyfile_scanner
391 env.Append(BUILDERS = {
392 'Doxygen': doxyfile_builder,
397 DOXYGENCOM = 'cd ${SOURCE.dir} && ${DOXYGEN} ${SOURCE.file}'
402 Make sure doxygen exists.
404 return env.Detect("doxygen")