Rename senfscons to site_scons and move python files arround
[senf.git] / site_scons / site_tools / Doxygen.py
1 # The Doxygen builder is based on the Doxygen builder from:
2 #
3 # Astxx, the Asterisk C++ API and Utility Library.
4 # Copyright (C) 2005, 2006  Matthew A. Nicholson
5 # Copyright (C) 2006  Tim Blechmann
6 #
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.
10 #
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.
15 #
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
19
20 # The Modifications are Copyright (C) 2006,2007
21 # Fraunhofer Institute for Open Communication Systems (FOKUS) 
22 # Competence Center NETwork research (NET), St. Augustin, GERMANY 
23 #     Stefan Bund <g0dil@berlios.de>
24
25 ## \file
26 # \brief Doxygen builder
27
28 ## \package senfscons.Doxygen
29 # \brief Doxygen Documentation Builder
30 #
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
34 # configuration file.
35 #
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
40 #     directives
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)
45 #
46 # The builder will emit a list of targets built by doxygen. This
47 # depends on the types of documentation built.
48 #
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
57 # tagfile used.
58 #
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>
63 # </table>
64 #
65 # \ingroup builder
66
67 # I (g0dil@berlios.de) have been fighting 4 problems in this
68 # implementation:
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
74 #   scons)
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
82 #   do).
83 #
84 # Together, these problems have produced several difficulties. I have
85 # solved them by
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
93 #
94 # You will find all this in the DoxyEmitter
95
96 import os, sys, traceback, string
97 import os.path
98 import glob, re
99 import SCons.Action
100 from fnmatch import fnmatch
101
102 class DoxyfileLexer:
103
104    def __init__(self,stream):
105       self._stream = stream
106       self._buffer = ""
107       self.lineno = 0
108       self._eof = False
109       self._fillbuffer()
110       
111    VARIABLE_RE = re.compile("[@A-Z_]+")
112    OPERATOR_RE = re.compile("\\+?=")
113    VALUE_RE = re.compile("\\S+")
114
115    def _readline(self):
116       if self._eof:
117          self._buffer = ""
118          return
119       self._buffer = self._stream.readline()
120       if not self._buffer:
121          self._eof = True
122          return
123       self._buffer = self._buffer.strip()
124       self.lineno += 1
125
126    def _skip(self, nchars=0):
127       self._buffer = self._buffer[nchars:].strip()
128       while self._buffer[:1] == '\\' and not self.eof():
129          self._readline()
130       if self._buffer[:1] == '#':
131          self._buffer=""
132       
133    def _fillbuffer(self):
134       while not self._buffer and not self.eof():
135          self._readline()
136          self._skip()
137
138    def _token(self, re, read=False):
139       if not self._buffer and read:
140          self._fillbuffer()
141       if not self._buffer:
142          return ""
143       m = re.match(self._buffer)
144       if m:
145          v = self._buffer[:m.end()]
146          self._skip(m.end())
147          return v
148       else:
149          raise ValueError,"Invalid input"
150
151    def var(self): return self._token(self.VARIABLE_RE, True)
152    def op(self): return self._token(self.OPERATOR_RE)
153
154    def next(self):
155       if not self._buffer:
156          raise StopIteration
157       if self._buffer[0] == '"':
158          return self._qstr()
159       m = self.VALUE_RE.match(self._buffer)
160       if m:
161          v = self._buffer[:m.end()]
162          self._skip(m.end())
163          return v
164       else:
165          raise ValueError
166
167    def __iter__(self):
168       return self
169
170    QSKIP_RE = re.compile("[^\\\"]+")
171    
172    def _qstr(self):
173       self._buffer = self._buffer[1:]
174       v = ""
175       while self._buffer:
176           m = self.QSKIP_RE.match(self._buffer)
177           if m:
178              v += self._buffer[:m.end()]
179              self._buffer = self._buffer[m.end():]
180           if self._buffer[:1] == '"':
181              self._skip(1)
182              return v
183           if self._buffer[:1] == '\\' and len(self._buffer)>1:
184              v += self._buffer[1]
185              self._buffer = self._buffer[2:]
186           else:
187              raise ValueError,"Unexpected charachter in string"
188       raise ValueError,"Unterminated string"
189
190    def eof(self):
191       return self._eof
192
193 class DoxyfileParser:
194
195    ENVVAR_RE = re.compile(r"\$\(([0-9A-Za-z_-]+)\)")
196
197    def __init__(self, path, env, include_path=None, items = None):
198       self._env = env
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 {}
203
204    def parse(self):
205       while True:
206          var = self._lexer.var()
207          if not var: break;
208          op = self._lexer.op()
209          value = [ self._envsub(v) for v in self._lexer ]
210          if not value:
211             raise ValueError,"Missing value in assignment"
212          if var[0] == '@':
213             self._meta(var,op,value)
214          elif op == '=':
215             self._items[var] = value
216          else:
217             self._items.setdefault(var,[]).extend(value)
218
219    def _envsub(self,value):
220       return self.ENVVAR_RE.sub(lambda m, env=self._env : str(env.get(m.group(1),"")), value)
221
222    def _meta(self, cmd, op, value):
223       m = '_'+cmd[1:]
224       try:
225          m = getattr(self,m)
226       except AttributeError:
227          raise ValueError,'Unknown meta command ' + cmd
228       m(op,value)
229
230    def _INCLUDE(self, op, value):
231       if len(value) != 1:
232          raise ValueError,"Invalid argument to @INCLUDE"
233       
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('@INCLUDE',[]).append(p)
238             parser = DoxyfileParser(p, self._env, self._include_path, self._items)
239             parser.parse()
240             return
241
242       raise ValueError,"@INCLUDE file not found"
243
244    def _INCLUDE_PATH(self, op, value):
245       self._include_path.extend(value)
246
247    def items(self):
248       return self._items
249
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():
254       return {}
255    ENV = {}
256    ENV.update(env.get("ENV",{}))
257    ENV.update(env.get("DOXYENV", {}))
258    parser = DoxyfileParser(file,ENV)
259    try:
260       parser.parse()
261    except ValueError, v:
262       print "WARNING: Error while parsing doxygen configuration '%s': %s" % (str(file),str(v))
263       return {}
264    data = parser.items()
265    for k,v in data.items():
266       if not v : del data[k]
267       elif k in ("LAYOUT_FILE", "INPUT", "FILE_PATTERNS", "EXCLUDE_PATTERNS", "@INCLUDE", "TAGFILES") : continue
268       elif len(v)==1 : data[k] = v[0]
269    return data
270
271 def DoxySourceScan(node, env, path):
272    """
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.
275    """
276    dep_add_keys = (
277       ('HTML', 'LAYOUT_FILE'), 
278       (None,   '@INCLUDE'), 
279       ('HTML', 'HTML_HEADER'), 
280       ('HTML', 'HTML_FOOTER'), 
281       (None,   'TAGFILES'), 
282       (None,   'INPUT_FILTER'),
283    )
284
285    output_formats = {
286       "HTML"  : ("YES", "html"),
287       "LATEX" : ("YES", "latex"),
288       "RTF"   : ("NO",  "rtf"),
289       "MAN"   : ("YES", "man"),
290       "XML"   : ("NO",  "xml"),
291    }
292
293    default_file_patterns = (
294       '*.c', '*.cc', '*.cxx', '*.cpp', '*.c++', '*.java', '*.ii', '*.ixx',
295       '*.ipp', '*.i++', '*.inl', '*.h', '*.hh ', '*.hxx', '*.hpp', '*.h++',
296       '*.idl', '*.odl', '*.cs', '*.php', '*.php3', '*.inc', '*.m', '*.mm',
297       '*.py',
298    )
299
300    default_exclude_patterns = (
301       '*~',
302    )
303
304    sources          = []
305    basedir          = node.dir.abspath
306    data             = DoxyfileParse(env, node.abspath)
307    recursive        = data.get("RECURSIVE", "NO").upper()=="YES"
308    file_patterns    = data.get("FILE_PATTERNS", default_file_patterns)
309    exclude_patterns = data.get("EXCLUDE_PATTERNS", default_exclude_patterns)
310
311    for i in data.get("INPUT", [ "." ]):
312       input = os.path.normpath(os.path.join(basedir,i))
313       if os.path.isfile(input):
314          sources.append(input)
315       elif os.path.isdir(input):
316          if recursive : entries = os.walk(input)
317          else         : entries = [ (input, [], os.listdir(input)) ]
318          for root, dirs, files in entries:
319             for f in files:
320                filename = os.path.normpath(os.path.join(root, f))
321                if (         reduce(lambda x, y: x or fnmatch(f, y), file_patterns,    False)
322                     and not reduce(lambda x, y: x or fnmatch(f, y), exclude_patterns, False) ):
323                   sources.append(filename)
324
325    for fmt, key in dep_add_keys:
326       if data.has_key(key) and \
327              (fmt is None or data.get("GENERATE_%s" % fmt, output_formats[fmt][0]).upper() == "YES"):
328          elt = env.Flatten(env.subst_list(data[key]))
329          sources.extend([ os.path.normpath(os.path.join(basedir,f))
330                           for f in elt if f ])
331
332    sources = map( lambda path: env.File(path), sources )
333    return sources
334
335 def DoxySourceScanCheck(node, env):
336    """Check if we should scan this file"""
337    return os.path.isfile(node.path)
338
339 def DoxyEmitter(source, target, env):
340    """Doxygen Doxyfile emitter"""
341    # possible output formats and their default values and output locations
342    output_formats = {
343       "HTML"  : ("YES", "html"),
344       "LATEX" : ("YES", "latex"),
345       "RTF"   : ("NO",  "rtf"),
346       "MAN"   : ("YES", "man"),
347       "XML"   : ("NO",  "xml"),
348    }
349
350    data = DoxyfileParse(env, source[0].abspath)
351
352    targets = []
353    if data.get("OUTPUT_DIRECTORY",""):
354       out_dir = data["OUTPUT_DIRECTORY"]
355       dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir) )
356       dir.sources = source
357       if env.GetOption('clean'):
358          targets.append(dir)
359          return (targets, source)
360    else:
361       out_dir = '.'
362
363    # add our output locations
364    html_dir = None
365    for (k, v) in output_formats.iteritems():
366       if data.get("GENERATE_" + k, v[0]).upper() == "YES" and data.get(k + "_OUTPUT", v[1]):
367          dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir, data.get(k + "_OUTPUT", v[1])) )
368          if k == "HTML" : html_dir = dir
369          dir.sources = source
370          node = env.File( os.path.join(dir.abspath, k.lower()+".stamp" ) )
371          targets.append(node)
372          if env.GetOption('clean'): targets.append(dir)
373
374    if data.get("GENERATE_TAGFILE",""):
375       targets.append(env.File( os.path.join(source[0].dir.abspath, data["GENERATE_TAGFILE"]) ))
376
377    if data.get("SEARCHENGINE","NO").upper() == "YES" and html_dir:
378       targets.append(env.File( os.path.join(html_dir.abspath, "search.idx") ))
379
380    # don't clobber targets
381    for node in targets:
382       env.Precious(node)
383
384    return (targets, source)
385
386 def doxyNodeHtmlDir(env,node):
387    if not node.sources : return None
388    data = DoxyfileParse(env, node.sources[0].abspath)
389    if data.get("GENERATE_HTML",'YES').upper() != 'YES' : return None
390    return os.path.normpath(os.path.join( node.sources[0].dir.abspath,
391                                          data.get("OUTPUT_DIRECTORY","."),
392                                          data.get("HTML_OUTPUT","html") ))
393
394 def relpath(source, target):
395    source = os.path.normpath(source)
396    target = os.path.normpath(target)
397    prefix = os.path.dirname(os.path.commonprefix((source,target)))
398    prefix_len = prefix and len(prefix.split(os.sep)) or 0
399    source_elts = source.split(os.sep)
400    target_elts = target.split(os.sep)
401    if source_elts[0] == '..' or target_elts[0] == '..':
402       raise ValueError, "invalid relapth args"
403    return os.path.join(*([".."] * (len(source_elts) - prefix_len) +
404                          target_elts[prefix_len:]))
405
406 def DoxyGenerator(source, target, env, for_signature):
407    data = DoxyfileParse(env, source[0].abspath)
408    actions = [ 
409       SCons.Action.Action("$DOXYGENCOM"),
410       SCons.Action.Action([ "touch $TARGETS" ]),
411       ]
412    
413    return actions
414
415 def generate(env):
416    """
417    Add builders and construction variables for the
418    Doxygen tool. This is currently for Doxygen 1.4.6.
419    """
420    doxyfile_scanner = env.Scanner(
421       DoxySourceScan,
422       "DoxySourceScan",
423       scan_check = DoxySourceScanCheck,
424    )
425
426    doxyfile_builder = env.Builder(
427       generator = DoxyGenerator,
428       emitter = DoxyEmitter,
429       target_factory = env.fs.Entry,
430       single_source = True,
431       source_scanner = doxyfile_scanner
432    )
433
434    env.Append(BUILDERS = {
435       'Doxygen': doxyfile_builder,
436    })
437
438    env.SetDefault(
439       DOXYGENCOM = 'cd ${SOURCE.dir} && doxygen ${SOURCE.file}'
440    )
441
442 def exists(env):
443    """
444    Make sure doxygen exists.
445    """
446    return env.Detect("doxygen")