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, path, env, include_path=None, items = None):
156 self._include_path = include_path or []
157 self._lexer = DoxyfileLexer(file(path))
158 self._dir = os.path.split(path)[0]
159 self._items = items or {}
163 var = self._lexer.var()
165 op = self._lexer.op()
166 value = [ self._envsub(v) for v in self._lexer ]
168 raise ValueError,"Missing value in assignment"
170 self._meta(var,op,value)
172 self._items[var] = value
174 self._items.setdefault(var,[]).extend(value)
176 def _envsub(self,value):
177 return self.ENVVAR_RE.sub(lambda m, env=self._env : str(env.get(m.group(1),"")), value)
179 def _meta(self, cmd, op, value):
183 except AttributeError:
184 raise ValueError,'Unknown meta command ' + cmd
187 def _INCLUDE(self, op, value):
189 raise ValueError,"Invalid argument to @INCLUDE"
191 for d in [ self._dir ] + self._include_path:
192 p = os.path.join(d,value[0])
193 if os.path.exists(p):
194 self._items.setdefault('@INCLUDE',[]).append(p)
195 parser = DoxyfileParser(p, self._env, self._include_path, self._items)
199 raise ValueError,"@INCLUDE file not found"
201 def _INCLUDE_PATH(self, op, value):
202 self._include_path.extend(value)
207 def DoxyfileParse(env,file):
208 # We don't parse source files which do not contain the word 'doxyfile'. SCons will
209 # pass other dependencies to DoxyfileParse which are not doxyfiles ... grmpf ...
210 if not 'doxyfile' in file.lower():
213 ENV.update(env.get("ENV",{}))
214 ENV.update(env.get("DOXYENV", {}))
215 parser = DoxyfileParser(file,ENV)
218 except ValueError, v:
219 print "WARNING: Error while parsing doxygen configuration '%s': %s" % (str(file),str(v))
221 data = parser.items()
222 for k,v in data.items():
223 if not v : del data[k]
224 elif k in ("LAYOUT_FILE", "INPUT", "FILE_PATTERNS", "EXCLUDE_PATTERNS", "@INCLUDE", "TAGFILES") : continue
225 elif len(v)==1 : data[k] = v[0]
228 def DoxySourceScan(node, env, path):
230 Doxygen Doxyfile source scanner. This should scan the Doxygen file and add
231 any files used to generate docs to the list of source files.
234 ('HTML', 'LAYOUT_FILE'),
236 ('HTML', 'HTML_HEADER'),
237 ('HTML', 'HTML_FOOTER'),
239 (None, 'INPUT_FILTER'),
243 "HTML" : ("YES", "html"),
244 "LATEX" : ("YES", "latex"),
245 "RTF" : ("NO", "rtf"),
246 "MAN" : ("YES", "man"),
247 "XML" : ("NO", "xml"),
250 default_file_patterns = (
251 '*.c', '*.cc', '*.cxx', '*.cpp', '*.c++', '*.java', '*.ii', '*.ixx',
252 '*.ipp', '*.i++', '*.inl', '*.h', '*.hh ', '*.hxx', '*.hpp', '*.h++',
253 '*.idl', '*.odl', '*.cs', '*.php', '*.php3', '*.inc', '*.m', '*.mm',
257 default_exclude_patterns = (
262 basedir = node.dir.abspath
263 data = DoxyfileParse(env, node.abspath)
264 recursive = data.get("RECURSIVE", "NO").upper()=="YES"
265 file_patterns = data.get("FILE_PATTERNS", default_file_patterns)
266 exclude_patterns = data.get("EXCLUDE_PATTERNS", default_exclude_patterns)
268 for i in data.get("INPUT", [ "." ]):
269 input = os.path.normpath(os.path.join(basedir,i))
270 if os.path.isfile(input):
271 sources.append(input)
272 elif os.path.isdir(input):
273 if recursive : entries = os.walk(input)
274 else : entries = [ (input, [], os.listdir(input)) ]
275 for root, dirs, files in entries:
277 filename = os.path.normpath(os.path.join(root, f))
278 if ( reduce(lambda x, y: x or fnmatch(f, y), file_patterns, False)
279 and not reduce(lambda x, y: x or fnmatch(f, y), exclude_patterns, False) ):
280 sources.append(filename)
282 for fmt, key in dep_add_keys:
283 if data.has_key(key) and \
284 (fmt is None or data.get("GENERATE_%s" % fmt, output_formats[fmt][0]).upper() == "YES"):
285 elt = env.Flatten(env.subst_list(data[key]))
286 sources.extend([ os.path.normpath(os.path.join(basedir,f))
289 sources = map( lambda path: env.File(path), sources )
292 def DoxySourceScanCheck(node, env):
293 """Check if we should scan this file"""
294 return os.path.isfile(node.path)
296 def DoxyEmitter(source, target, env):
297 """Doxygen Doxyfile emitter"""
298 # possible output formats and their default values and output locations
300 "HTML" : ("YES", "html"),
301 "LATEX" : ("YES", "latex"),
302 "RTF" : ("NO", "rtf"),
303 "MAN" : ("YES", "man"),
304 "XML" : ("NO", "xml"),
307 data = DoxyfileParse(env, source[0].abspath)
310 if data.get("OUTPUT_DIRECTORY",""):
311 out_dir = data["OUTPUT_DIRECTORY"]
312 dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir) )
314 if env.GetOption('clean'):
316 return (targets, source)
320 # add our output locations
322 for (k, v) in output_formats.iteritems():
323 if data.get("GENERATE_" + k, v[0]).upper() == "YES" and data.get(k + "_OUTPUT", v[1]):
324 dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir, data.get(k + "_OUTPUT", v[1])) )
325 if k == "HTML" : html_dir = dir
327 node = env.File( os.path.join(dir.abspath, k.lower()+".stamp" ) )
329 if env.GetOption('clean'): targets.append(dir)
331 if data.get("GENERATE_TAGFILE",""):
332 targets.append(env.File( os.path.join(source[0].dir.abspath, data["GENERATE_TAGFILE"]) ))
334 if data.get("SEARCHENGINE","NO").upper() == "YES" and html_dir:
335 targets.append(env.File( os.path.join(html_dir.abspath, "search.idx") ))
337 # don't clobber targets
341 return (targets, source)
343 def doxyNodeHtmlDir(env,node):
344 if not node.sources : return None
345 data = DoxyfileParse(env, node.sources[0].abspath)
346 if data.get("GENERATE_HTML",'YES').upper() != 'YES' : return None
347 return os.path.normpath(os.path.join( node.sources[0].dir.abspath,
348 data.get("OUTPUT_DIRECTORY","."),
349 data.get("HTML_OUTPUT","html") ))
351 def relpath(source, target):
352 source = os.path.normpath(source)
353 target = os.path.normpath(target)
354 prefix = os.path.dirname(os.path.commonprefix((source,target)))
355 prefix_len = prefix and len(prefix.split(os.sep)) or 0
356 source_elts = source.split(os.sep)
357 target_elts = target.split(os.sep)
358 if source_elts[0] == '..' or target_elts[0] == '..':
359 raise ValueError, "invalid relapth args"
360 return os.path.join(*([".."] * (len(source_elts) - prefix_len) +
361 target_elts[prefix_len:]))
363 def DoxyGenerator(source, target, env, for_signature):
364 data = DoxyfileParse(env, source[0].abspath)
366 SCons.Action.Action("$DOXYGENCOM"),
367 SCons.Action.Action([ "touch $TARGETS" ]),
374 Add builders and construction variables for the
375 Doxygen tool. This is currently for Doxygen 1.4.6.
377 doxyfile_scanner = env.Scanner(
380 scan_check = DoxySourceScanCheck,
383 doxyfile_builder = env.Builder(
384 generator = DoxyGenerator,
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,
396 DOXYGENCOM = 'cd ${SOURCE.dir} && doxygen ${SOURCE.file}'
401 Make sure doxygen exists.
403 return env.Detect("doxygen")