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
21 # Fraunhofer Institut fuer offene Kommunikationssysteme (FOKUS)
22 # Kompetenzzentrum fuer Satelitenkommunikation (SatCom)
23 # Stefan Bund <g0dil@berlios.de>
26 # \brief Doxygen builder
28 ## \package senfscons.Doxygen
29 # \brief Doxygen Documentation Builder
31 # This builder will invoke \c doxygen to build software
32 # documentation. The doxygen builder only takes the name of the
33 # doxyfile as it's source file. The builder parses that doxygen
36 # The builder will automatically find all sources on which the
37 # documentation depends. This includes
38 # \li the source code files (as selected by the \c RECURSIVE, \c
39 # FILE_PATTERNS, \c INPUT and \c EXCLUDE_PATTERNS doxygen
41 # \li the \c HTML_HEADER and \c HTML_FOOTER
42 # \li all referenced \c TAGFILES
43 # \li the \c INPUT_FILTER
44 # \li all included doxyfiles (via \c @INCLUDE)
46 # The builder will emit a list of targets built by doxygen. This
47 # depends on the types of documentation built.
49 # The builder will also generate additional commands to resolve
50 # cross-references to other module documentations. This is based on
51 # the \c TAGFILES used. Tagfiles built in the same project in other
52 # modules are automatically found and the links will be resolved
53 # correctly. To resolve links from external tagfiles, you may specify
54 # <i>tagfilename</i><tt>_DOXY_URL</tt> as a construction environment
55 # variable to specify the path to resolve references from the given
56 # tagfile to. <i>tagfilename</i> is the uppercased basename of the
59 # \par Construction Envrionment Variables:
60 # <table class="senf">
61 # <tr><td>\c DOXYGEN</td><td>doxygen command, defaults to \c doxygen</td></tr>
62 # <tr><td><i>tag</i><tt>_DOXY_URL</tt></td><td>external tagfile resolve URL</td></tr>
67 # I (g0dil@berlios.de) have been fighting 4 problems in this
69 # - A Directory target will *not* call any source scanners
70 # - A Directory target will interpret the directory contents as
71 # sources not targets. This means, that if a command creates that
72 # directory plus contents, the target will never be up-to-date
73 # (since the directory contents will change with every call of
75 # - Theres a bug in SCons which will produce an error message for
76 # directory targets if dir.sources is not set explicitly
77 # - the first argument to env.Clean() must be the command line target,
78 # with which the scons was invoked. This does not help to add
79 # aditional files or directories to be cleaned if you don't know
80 # that target (it's not possible to say 'if you clean this file,
81 # also clean that one' hich is, what I had expected env.Clean to
84 # Together, these problems have produced several difficulties. I have
86 # - Adding an (empty) stamp file as a (file) target. This target will
87 # cause source scanners to be invoked
88 # - Adding the documentation directory as a target (so it will be
89 # cleaned up which env.Clean doesn't help me to do), but *only* if
90 # scons is called to with the -c option
91 # - Setting dir.sources to the known source-list to silence the error
92 # message whenever a directory is added as a target
94 # You will find all this in the DoxyEmitter
96 import os, sys, traceback, string
100 from fnmatch import fnmatch
104 def __init__(self,stream):
105 self._stream = stream
111 VARIABLE_RE = re.compile("[@A-Z_]+")
112 OPERATOR_RE = re.compile("\\+?=")
113 VALUE_RE = re.compile("\\S+")
119 self._buffer = self._stream.readline()
123 self._buffer = self._buffer.strip()
126 def _skip(self, nchars=0):
127 self._buffer = self._buffer[nchars:].strip()
128 while self._buffer[:1] == '\\' and not self.eof():
130 if self._buffer[:1] == '#':
133 def _fillbuffer(self):
134 while not self._buffer and not self.eof():
138 def _token(self, re, read=False):
139 if not self._buffer and read:
143 m = re.match(self._buffer)
145 v = self._buffer[:m.end()]
149 raise ValueError,"Invalid input"
151 def var(self): return self._token(self.VARIABLE_RE, True)
152 def op(self): return self._token(self.OPERATOR_RE)
157 if self._buffer[0] == '"':
159 m = self.VALUE_RE.match(self._buffer)
161 v = self._buffer[:m.end()]
170 QSKIP_RE = re.compile("[^\\\"]+")
173 self._buffer = self._buffer[1:]
176 m = self.QSKIP_RE.match(self._buffer)
178 v += self._buffer[:m.end()]
179 self._buffer = self._buffer[m.end():]
180 if self._buffer[:1] == '"':
183 if self._buffer[:1] == '\\' and len(self._buffer)>1:
185 self._buffer = self._buffer[2:]
187 raise ValueError,"Unexpected charachter in string"
188 raise ValueError,"Unterminated string"
193 class DoxyfileParser:
195 ENVVAR_RE = re.compile(r"\$\(([0-9A-Za-z_-]+)\)")
197 def __init__(self, path, env, include_path=None, items = None):
199 self._include_path = include_path or []
200 self._lexer = DoxyfileLexer(file(path))
201 self._dir = os.path.split(path)[0]
202 self._items = items or {}
206 var = self._lexer.var()
208 op = self._lexer.op()
209 value = [ self._envsub(v) for v in self._lexer ]
211 raise ValueError,"Missing value in assignment"
213 self._meta(var,op,value)
215 self._items[var] = value
217 self._items.setdefault(var,[]).extend(value)
219 def _envsub(self,value):
220 return self.ENVVAR_RE.sub(lambda m, env=self._env : str(env.get(m.group(1),"")), value)
222 def _meta(self, cmd, op, value):
226 except AttributeError:
227 raise ValueError,'Unknown meta command ' + cmd
230 def _INCLUDE(self, op, value):
232 raise ValueError,"Invalid argument to @INCLUDE"
234 for d in [ self._dir ] + self._include_path:
235 p = os.path.join(d,value[0])
236 if os.path.exists(p):
237 self._items.setdefault('@INCLDUE',[]).append(p)
238 parser = DoxyfileParser(p, self._env, self._include_path, self._items)
242 raise ValueError,"@INCLUDE file not found"
244 def _INCLUDE_PATH(self, op, value):
245 self._include_path.extend(value)
250 def DoxyfileParse(env,file):
251 # We don't parse source files which do not contain the word 'doxyfile'. SCons will
252 # pass other dependencies to DoxyfileParse which are not doxyfiles ... grmpf ...
253 if not 'doxyfile' in file.lower():
256 ENV.update(env.get("ENV",{}))
257 ENV['TOPDIR'] = env.Dir('#').abspath
258 parser = DoxyfileParser(file,ENV)
261 except ValueError, v:
262 print "WARNING: Error while parsing doxygen configuration '%s': %s" % (str(file),str(v))
264 data = parser.items()
265 for k,v in data.items():
266 if not v : del data[k]
267 elif k in ("INPUT", "FILE_PATTERNS", "EXCLUDE_PATTERNS", "@INCLUDE", "TAGFILES") : continue
268 elif len(v)==1 : data[k] = v[0]
271 def DoxySourceScan(node, env, path):
273 Doxygen Doxyfile source scanner. This should scan the Doxygen file and add
274 any files used to generate docs to the list of source files.
277 '@INCLUDE', 'HTML_HEADER', 'HTML_FOOTER', 'TAGFILES', 'INPUT_FILTER'
280 default_file_patterns = (
281 '*.c', '*.cc', '*.cxx', '*.cpp', '*.c++', '*.java', '*.ii', '*.ixx',
282 '*.ipp', '*.i++', '*.inl', '*.h', '*.hh ', '*.hxx', '*.hpp', '*.h++',
283 '*.idl', '*.odl', '*.cs', '*.php', '*.php3', '*.inc', '*.m', '*.mm',
287 default_exclude_patterns = (
292 basedir = node.dir.abspath
293 data = DoxyfileParse(env, node.abspath)
294 recursive = data.get("RECURSIVE", "NO").upper()=="YES"
295 file_patterns = data.get("FILE_PATTERNS", default_file_patterns)
296 exclude_patterns = data.get("EXCLUDE_PATTERNS", default_exclude_patterns)
298 for i in data.get("INPUT", [ "." ]):
299 input = os.path.normpath(os.path.join(basedir,i))
300 if os.path.isfile(input):
301 sources.append(input)
302 elif os.path.isdir(input):
303 if recursive : entries = os.walk(input)
304 else : entries = [ (input, [], os.listdir(input)) ]
305 for root, dirs, files in entries:
307 filename = os.path.normpath(os.path.join(root, f))
308 if ( reduce(lambda x, y: x or fnmatch(f, y),
309 file_patterns, False)
310 and not reduce(lambda x, y: x or fnmatch(f, y),
311 exclude_patterns, False) ):
312 sources.append(filename)
314 for key in dep_add_keys:
315 if data.has_key(key):
317 if type(elt) is type ("") : elt = [ elt ]
318 sources.extend([ os.path.normpath(os.path.join(basedir,f))
321 sources = map( lambda path: env.File(path), sources )
324 def DoxySourceScanCheck(node, env):
325 """Check if we should scan this file"""
326 return os.path.isfile(node.path)
328 def DoxyEmitter(source, target, env):
329 """Doxygen Doxyfile emitter"""
330 # possible output formats and their default values and output locations
332 "HTML" : ("YES", "html"),
333 "LATEX" : ("YES", "latex"),
334 "RTF" : ("NO", "rtf"),
335 "MAN" : ("YES", "man"),
336 "XML" : ("NO", "xml"),
339 data = DoxyfileParse(env, source[0].abspath)
342 if data.has_key("OUTPUT_DIRECTORY"):
343 out_dir = data["OUTPUT_DIRECTORY"]
344 dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir) )
346 if env.GetOption('clean'): targets.append(dir)
350 # add our output locations
352 for (k, v) in output_formats.iteritems():
353 if data.get("GENERATE_" + k, v[0]).upper() == "YES":
354 dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir, data.get(k + "_OUTPUT", v[1])) )
355 if k == "HTML" : html_dir = dir
357 node = env.File( os.path.join(dir.abspath, k.lower()+".stamp" ) )
359 if env.GetOption('clean'): targets.append(dir)
361 if data.has_key("GENERATE_TAGFILE") and html_dir:
362 targets.append(env.File( os.path.join(source[0].dir.abspath, data["GENERATE_TAGFILE"]) ))
364 if data.get("SEARCHENGINE","NO").upper() == "YES":
365 targets.append(env.File( os.path.join(html_dir.abspath, "search.idx") ))
367 # don't clobber targets
371 return (targets, source)
373 def doxyNodeHtmlDir(env,node):
374 if not node.sources : return None
375 data = DoxyfileParse(env, node.sources[0].abspath)
376 if data.get("GENERATE_HTML",'YES').upper() != 'YES' : return None
377 return os.path.normpath(os.path.join( node.sources[0].dir.abspath,
378 data.get("OUTPUT_DIRECTORY","."),
379 data.get("HTML_OUTPUT","html") ))
381 def relpath(source, target):
382 source = os.path.normpath(source)
383 target = os.path.normpath(target)
384 prefix = os.path.dirname(os.path.commonprefix((source,target)))
385 prefix_len = prefix and len(prefix.split(os.sep)) or 0
386 source_elts = source.split(os.sep)
387 target_elts = target.split(os.sep)
388 if source_elts[0] == '..' or target_elts[0] == '..':
389 raise ValueError, "invalid relapth args"
390 return os.path.join(*([".."] * (len(source_elts) - prefix_len) +
391 target_elts[prefix_len:]))
393 def DoxyGenerator(source, target, env, for_signature):
395 data = DoxyfileParse(env, source[0].abspath)
397 actions = [ SCons.Action.Action("cd ${SOURCE.dir} && TOPDIR=%s ${DOXYGEN} ${SOURCE.file}"
398 % (relpath(source[0].dir.abspath, env.Dir('#').abspath),)) ]
400 # This will add automatic 'installdox' calls.
402 # For every referenced tagfile, the generator first checks for the
403 # existence of a construction variable '<name>_DOXY_URL' where
404 # '<name>' is the uppercased name of the tagfile sans extension
405 # (e.g. 'Utils.tag' -> 'UTILS_DOXY_URL'). If this variable exists,
406 # it must contain the url or path to the installed documentation
407 # corresponding to the tag file.
409 # Is the variable is not found and if a referenced tag file is a
410 # target within this same build, the generator will parse the
411 # 'Doxyfile' from which the tag file is built. It will
412 # automatically create the html directory from the information in
415 # If for any referenced tagfile no url can be found, 'installdox'
416 # will *not* be called and a warning about the missing url is
419 if data.get('GENERATE_HTML','YES').upper() == "YES":
420 output_dir = os.path.normpath(os.path.join( source[0].dir.abspath,
421 data.get("OUTPUT_DIRECTORY","."),
422 data.get("HTML_OUTPUT","html") ))
424 for tagfile in data.get('TAGFILES',[]):
425 url = env.get(os.path.splitext(os.path.basename(tagfile))[0].upper()+"_DOXY_URL", None)
427 url = doxyNodeHtmlDir(
429 env.File(os.path.normpath(os.path.join(str(source[0].dir), tagfile))))
430 if url : url = relpath(output_dir, url)
432 print "WARNING:",source[0].abspath, ": missing tagfile url for", tagfile
434 if args is not None and url:
435 args.append("-l %s@%s" % ( os.path.basename(tagfile), url ))
437 actions.append(SCons.Action.Action('cd %s && ./installdox %s' % (output_dir, " ".join(args))))
439 actions.append(SCons.Action.Action([ "touch $TARGETS" ]))
445 Add builders and construction variables for the
446 Doxygen tool. This is currently for Doxygen 1.4.6.
448 doxyfile_scanner = env.Scanner(
451 scan_check = DoxySourceScanCheck,
454 doxyfile_builder = env.Builder(
455 # scons 0.96.93 hang on the next line but I don't know hot to FIX the problem
456 generator = DoxyGenerator,
457 emitter = DoxyEmitter,
458 target_factory = env.fs.Entry,
459 single_source = True,
460 source_scanner = doxyfile_scanner
463 env.Append(BUILDERS = {
464 'Doxygen': doxyfile_builder,
473 Make sure doxygen exists.
475 return env.Detect("doxygen")