2de1bee5eded8fcda3bf64a9973e512aea9f3f3d
[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('@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       'LAYOUT_FILE', '@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), file_patterns,    False)
309                     and not reduce(lambda x, y: x or fnmatch(f, y), exclude_patterns, False) ):
310                   sources.append(filename)
311
312    for key in dep_add_keys:
313       if data.has_key(key):
314          elt = env.Flatten(env.subst_list(data[key]))
315          sources.extend([ os.path.normpath(os.path.join(basedir,f))
316                           for f in elt if f ])
317
318    sources = map( lambda path: env.File(path), sources )
319    return sources
320
321 def DoxySourceScanCheck(node, env):
322    """Check if we should scan this file"""
323    return os.path.isfile(node.path)
324
325 def DoxyEmitter(source, target, env):
326    """Doxygen Doxyfile emitter"""
327    # possible output formats and their default values and output locations
328    output_formats = {
329       "HTML"  : ("YES", "html"),
330       "LATEX" : ("YES", "latex"),
331       "RTF"   : ("NO",  "rtf"),
332       "MAN"   : ("YES", "man"),
333       "XML"   : ("NO",  "xml"),
334    }
335
336    data = DoxyfileParse(env, source[0].abspath)
337
338    targets = []
339    if data.get("OUTPUT_DIRECTORY",""):
340       out_dir = data["OUTPUT_DIRECTORY"]
341       dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir) )
342       dir.sources = source
343       if env.GetOption('clean'):
344          targets.append(dir)
345          return (targets, source)
346    else:
347       out_dir = '.'
348
349    # add our output locations
350    html_dir = None
351    for (k, v) in output_formats.iteritems():
352       if data.get("GENERATE_" + k, v[0]).upper() == "YES" and data.get(k + "_OUTPUT", v[1]):
353          dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir, data.get(k + "_OUTPUT", v[1])) )
354          if k == "HTML" : html_dir = dir
355          dir.sources = source
356          node = env.File( os.path.join(dir.abspath, k.lower()+".stamp" ) )
357          targets.append(node)
358          if env.GetOption('clean'): targets.append(dir)
359
360    if data.get("GENERATE_TAGFILE",""):
361       targets.append(env.File( os.path.join(source[0].dir.abspath, data["GENERATE_TAGFILE"]) ))
362
363    if data.get("SEARCHENGINE","NO").upper() == "YES" and html_dir:
364       targets.append(env.File( os.path.join(html_dir.abspath, "search.idx") ))
365
366    # don't clobber targets
367    for node in targets:
368       env.Precious(node)
369
370    return (targets, source)
371
372 def doxyNodeHtmlDir(env,node):
373    if not node.sources : return None
374    data = DoxyfileParse(env, node.sources[0].abspath)
375    if data.get("GENERATE_HTML",'YES').upper() != 'YES' : return None
376    return os.path.normpath(os.path.join( node.sources[0].dir.abspath,
377                                          data.get("OUTPUT_DIRECTORY","."),
378                                          data.get("HTML_OUTPUT","html") ))
379
380 def relpath(source, target):
381    source = os.path.normpath(source)
382    target = os.path.normpath(target)
383    prefix = os.path.dirname(os.path.commonprefix((source,target)))
384    prefix_len = prefix and len(prefix.split(os.sep)) or 0
385    source_elts = source.split(os.sep)
386    target_elts = target.split(os.sep)
387    if source_elts[0] == '..' or target_elts[0] == '..':
388       raise ValueError, "invalid relapth args"
389    return os.path.join(*([".."] * (len(source_elts) - prefix_len) +
390                          target_elts[prefix_len:]))
391
392 def DoxyGenerator(source, target, env, for_signature):
393    data = DoxyfileParse(env, source[0].abspath)
394    actions = [ 
395       SCons.Action.Action("$DOXYGENCOM"),
396       SCons.Action.Action([ "touch $TARGETS" ]),
397       ]
398    
399    return actions
400
401 def generate(env):
402    """
403    Add builders and construction variables for the
404    Doxygen tool. This is currently for Doxygen 1.4.6.
405    """
406    doxyfile_scanner = env.Scanner(
407       DoxySourceScan,
408       "DoxySourceScan",
409       scan_check = DoxySourceScanCheck,
410    )
411
412    doxyfile_builder = env.Builder(
413       generator = DoxyGenerator,
414       emitter = DoxyEmitter,
415       target_factory = env.fs.Entry,
416       single_source = True,
417       source_scanner = doxyfile_scanner
418    )
419
420    env.Append(BUILDERS = {
421       'Doxygen': doxyfile_builder,
422    })
423
424    env.SetDefault(
425       DOXYGENCOM = 'cd ${SOURCE.dir} && doxygen ${SOURCE.file}'
426    )
427
428 def exists(env):
429    """
430    Make sure doxygen exists.
431    """
432    return env.Detect("doxygen")