NEW FILE HEADER / COPYRIGHT FORMAT
[senf.git] / senfscons / 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('@INCLDUE',[]).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['TOPDIR'] = env.Dir('#').abspath
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 ("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       '@INCLUDE', 'HTML_HEADER', 'HTML_FOOTER', 'TAGFILES', 'INPUT_FILTER'
278    )
279
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',
284       '*.py',
285    )
286
287    default_exclude_patterns = (
288       '*~',
289    )
290
291    sources          = []
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)
297
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:
306             for f in files:
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)
313
314    for key in dep_add_keys:
315       if data.has_key(key):
316          elt = data[key]
317          if type(elt) is type ("") : elt = [ elt ]
318          sources.extend([ os.path.normpath(os.path.join(basedir,f))
319                           for f in elt ])
320
321    sources = map( lambda path: env.File(path), sources )
322    return sources
323
324 def DoxySourceScanCheck(node, env):
325    """Check if we should scan this file"""
326    return os.path.isfile(node.path)
327
328 def DoxyEmitter(source, target, env):
329    """Doxygen Doxyfile emitter"""
330    # possible output formats and their default values and output locations
331    output_formats = {
332       "HTML"  : ("YES", "html"),
333       "LATEX" : ("YES", "latex"),
334       "RTF"   : ("NO",  "rtf"),
335       "MAN"   : ("YES", "man"),
336       "XML"   : ("NO",  "xml"),
337    }
338
339    data = DoxyfileParse(env, source[0].abspath)
340
341    targets = []
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) )
345       dir.sources = source
346       if env.GetOption('clean'):
347          targets.append(dir)
348          return (targets, source)
349    else:
350       out_dir = '.'
351
352    # add our output locations
353    html_dir = None
354    for (k, v) in output_formats.iteritems():
355       if data.get("GENERATE_" + k, v[0]).upper() == "YES":
356          dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir, data.get(k + "_OUTPUT", v[1])) )
357          if k == "HTML" : html_dir = dir
358          dir.sources = source
359          node = env.File( os.path.join(dir.abspath, k.lower()+".stamp" ) )
360          targets.append(node)
361          if env.GetOption('clean'): targets.append(dir)
362
363    if data.has_key("GENERATE_TAGFILE") and html_dir:
364       targets.append(env.File( os.path.join(source[0].dir.abspath, data["GENERATE_TAGFILE"]) ))
365
366    if data.get("SEARCHENGINE","NO").upper() == "YES":
367       targets.append(env.File( os.path.join(html_dir.abspath, "search.idx") ))
368
369    # don't clobber targets
370    for node in targets:
371       env.Precious(node)
372
373    return (targets, source)
374
375 def doxyNodeHtmlDir(env,node):
376    if not node.sources : return None
377    data = DoxyfileParse(env, node.sources[0].abspath)
378    if data.get("GENERATE_HTML",'YES').upper() != 'YES' : return None
379    return os.path.normpath(os.path.join( node.sources[0].dir.abspath,
380                                          data.get("OUTPUT_DIRECTORY","."),
381                                          data.get("HTML_OUTPUT","html") ))
382
383 def relpath(source, target):
384    source = os.path.normpath(source)
385    target = os.path.normpath(target)
386    prefix = os.path.dirname(os.path.commonprefix((source,target)))
387    prefix_len = prefix and len(prefix.split(os.sep)) or 0
388    source_elts = source.split(os.sep)
389    target_elts = target.split(os.sep)
390    if source_elts[0] == '..' or target_elts[0] == '..':
391       raise ValueError, "invalid relapth args"
392    return os.path.join(*([".."] * (len(source_elts) - prefix_len) +
393                          target_elts[prefix_len:]))
394
395 def DoxyGenerator(source, target, env, for_signature):
396
397    data = DoxyfileParse(env, source[0].abspath)
398
399    actions = [ SCons.Action.Action("cd ${SOURCE.dir}  && TOPDIR=%s ${DOXYGEN} ${SOURCE.file}"
400                                    % (relpath(source[0].dir.abspath, env.Dir('#').abspath),)) ]
401
402    # This will add automatic 'installdox' calls.
403    #
404    # For every referenced tagfile, the generator first checks for the
405    # existence of a construction variable '<name>_DOXY_URL' where
406    # '<name>' is the uppercased name of the tagfile sans extension
407    # (e.g. 'Utils.tag' -> 'UTILS_DOXY_URL'). If this variable exists,
408    # it must contain the url or path to the installed documentation
409    # corresponding to the tag file.
410    #
411    # Is the variable is not found and if a referenced tag file is a
412    # target within this same build, the generator will parse the
413    # 'Doxyfile' from which the tag file is built. It will
414    # automatically create the html directory from the information in
415    # that 'Doxyfile'.
416    #
417    # If for any referenced tagfile no url can be found, 'installdox'
418    # will *not* be called and a warning about the missing url is
419    # generated.
420
421    if data.get('GENERATE_HTML','YES').upper() == "YES":
422       output_dir = os.path.normpath(os.path.join( source[0].dir.abspath,
423                                                   data.get("OUTPUT_DIRECTORY","."),
424                                                   data.get("HTML_OUTPUT","html") ))
425       args = []
426       for tagfile in data.get('TAGFILES',[]):
427          url = env.get(os.path.splitext(os.path.basename(tagfile))[0].upper()+"_DOXY_URL", None)
428          if not url:
429             url = doxyNodeHtmlDir(
430                env,
431                env.File(os.path.normpath(os.path.join(str(source[0].dir), tagfile))))
432             if url : url = relpath(output_dir, url)
433          if not url:
434             print "WARNING:",source[0].abspath, ": missing tagfile url for", tagfile
435             args = None
436          if args is not None and url:
437             args.append("-l %s@%s" % ( os.path.basename(tagfile), url ))
438       if args:
439          actions.append(SCons.Action.Action('cd %s && ./installdox %s' % (output_dir, " ".join(args))))
440
441    actions.append(SCons.Action.Action([ "touch $TARGETS" ]))
442
443    return actions
444
445 def generate(env):
446    """
447    Add builders and construction variables for the
448    Doxygen tool. This is currently for Doxygen 1.4.6.
449    """
450    doxyfile_scanner = env.Scanner(
451       DoxySourceScan,
452       "DoxySourceScan",
453       scan_check = DoxySourceScanCheck,
454    )
455
456    doxyfile_builder = env.Builder(
457       # scons 0.96.93 hang on the next line but I don't know hot to FIX the problem
458       generator = DoxyGenerator,
459       emitter = DoxyEmitter,
460       target_factory = env.fs.Entry,
461       single_source = True,
462       source_scanner =  doxyfile_scanner
463    )
464
465    env.Append(BUILDERS = {
466       'Doxygen': doxyfile_builder,
467    })
468
469    env.AppendUnique(
470       DOXYGEN = 'doxygen',
471    )
472
473 def exists(env):
474    """
475    Make sure doxygen exists.
476    """
477    return env.Detect("doxygen")