e79cfc96b9271b384f3c6d2966b0eea5e0b66bea
[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,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>
24
25 # I (g0dil@berlios.de) have been fighting 4 problems in this
26 # implementation:
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
32 #   scons)
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).
40 #
41 # Together, these problems have produced several difficulties. I have
42 # solved them by
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
50 #
51 # You will find all this in the DoxyEmitter
52
53 import os, sys, traceback, string
54 import os.path
55 import glob, re
56 import SCons.Action
57 from fnmatch import fnmatch
58
59 class DoxyfileLexer:
60
61    def __init__(self,stream):
62       self._stream = stream
63       self._buffer = ""
64       self.lineno = 0
65       self._eof = False
66       self._fillbuffer()
67       
68    VARIABLE_RE = re.compile("[@A-Z_]+")
69    OPERATOR_RE = re.compile("\\+?=")
70    VALUE_RE = re.compile("\\S+")
71
72    def _readline(self):
73       if self._eof:
74          self._buffer = ""
75          return
76       self._buffer = self._stream.readline()
77       if not self._buffer:
78          self._eof = True
79          return
80       self._buffer = self._buffer.strip()
81       self.lineno += 1
82
83    def _skip(self, nchars=0):
84       self._buffer = self._buffer[nchars:].strip()
85       while self._buffer[:1] == '\\' and not self.eof():
86          self._readline()
87       if self._buffer[:1] == '#':
88          self._buffer=""
89       
90    def _fillbuffer(self):
91       while not self._buffer and not self.eof():
92          self._readline()
93          self._skip()
94
95    def _token(self, re, read=False):
96       if not self._buffer and read:
97          self._fillbuffer()
98       if not self._buffer:
99          return ""
100       m = re.match(self._buffer)
101       if m:
102          v = self._buffer[:m.end()]
103          self._skip(m.end())
104          return v
105       else:
106          raise ValueError,"Invalid input"
107
108    def var(self): return self._token(self.VARIABLE_RE, True)
109    def op(self): return self._token(self.OPERATOR_RE)
110
111    def next(self):
112       if not self._buffer:
113          raise StopIteration
114       if self._buffer[0] == '"':
115          return self._qstr()
116       m = self.VALUE_RE.match(self._buffer)
117       if m:
118          v = self._buffer[:m.end()]
119          self._skip(m.end())
120          return v
121       else:
122          raise ValueError
123
124    def __iter__(self):
125       return self
126
127    QSKIP_RE = re.compile("[^\\\"]+")
128    
129    def _qstr(self):
130       self._buffer = self._buffer[1:]
131       v = ""
132       while self._buffer:
133           m = self.QSKIP_RE.match(self._buffer)
134           if m:
135              v += self._buffer[:m.end()]
136              self._buffer = self._buffer[m.end():]
137           if self._buffer[:1] == '"':
138              self._skip(1)
139              return v
140           if self._buffer[:1] == '\\' and len(self._buffer)>1:
141              v += self._buffer[1]
142              self._buffer = self._buffer[2:]
143           else:
144              raise ValueError,"Unexpected charachter in string"
145       raise ValueError,"Unterminated string"
146
147    def eof(self):
148       return self._eof
149
150 class DoxyfileParser:
151
152    ENVVAR_RE = re.compile(r"\$\(([0-9A-Za-z_-]+)\)")
153
154    def __init__(self, node, env, include_path=None, items = None):
155       self._node = node
156       self._env = env
157       self._include_path = include_path or []
158       self._lexer = DoxyfileLexer(file(node.srcnode().get_path()))
159       self._dir = node.dir
160       self._items = items or {}
161
162    def parse(self):
163       while True:
164          var = self._lexer.var()
165          if not var: break;
166          op = self._lexer.op()
167          value = [ self._envsub(v) for v in self._lexer ]
168          if not value:
169             raise ValueError,"Missing value in assignment"
170          if var[0] == '@':
171             self._meta(var,op,value)
172          elif op == '=':
173             self._items[var] = value
174          else:
175             self._items.setdefault(var,[]).extend(value)
176
177    def _envsub(self,value):
178       return self.ENVVAR_RE.sub(lambda m, env=self._env : str(env.get(m.group(1),"")), value)
179
180    def _meta(self, cmd, op, value):
181       m = '_'+cmd[1:]
182       try:
183          m = getattr(self,m)
184       except AttributeError:
185          raise ValueError,'Unknown meta command ' + cmd
186       m(op,value)
187
188    def _INCLUDE(self, op, value):
189       if len(value) != 1:
190          raise ValueError,"Invalid argument to @INCLUDE"
191       
192       for d in [ self._dir.get_path() ] + self._include_path:
193          p = os.path.join(d,value[0])
194          if os.path.exists(p):
195             self._items.setdefault('@INCLUDE',[]).append(p)
196             parser = DoxyfileParser(self._node.dir.File(p), self._env, self._include_path, self._items)
197             parser.parse()
198             return
199
200       raise ValueError,"@INCLUDE file '%s' not found" % value[0]
201
202    def _INCLUDE_PATH(self, op, value):
203       self._include_path.extend(value)
204
205    def items(self):
206       return self._items
207
208 def DoxyfileParse(env,node):
209    # We don't parse source files which do not contain the word 'doxyfile'. SCons will
210    # pass other dependencies to DoxyfileParse which are not doxyfiles ... grmpf ...
211    ENV = {}
212    ENV.update(env.get("ENV",{}))
213    ENV.update(env.get("DOXYENV", {}))
214    parser = DoxyfileParser(node,ENV)
215    try:
216       parser.parse()
217    except ValueError, v:
218       print "WARNING: Error while parsing doxygen configuration '%s': %s" % (str(file),str(v))
219       return {}
220    data = parser.items()
221    for k,v in data.items():
222       if not v : del data[k]
223       elif k in ("LAYOUT_FILE", "INPUT", "FILE_PATTERNS", "EXCLUDE_PATTERNS", "@INCLUDE", "TAGFILES") : continue
224       elif len(v)==1 : data[k] = v[0]
225    return data
226
227 def DoxySourceScan(node, env, path):
228    """
229    Doxygen Doxyfile source scanner.  This should scan the Doxygen file and add
230    any files used to generate docs to the list of source files.
231    """
232    dep_add_keys = (
233       ('HTML', 'LAYOUT_FILE'), 
234       (None,   '@INCLUDE'), 
235       ('HTML', 'HTML_HEADER'), 
236       ('HTML', 'HTML_FOOTER'), 
237       (None,   'TAGFILES'), 
238       (None,   'INPUT_FILTER'),
239    )
240
241    output_formats = {
242       "HTML"  : ("YES", "html"),
243       "LATEX" : ("YES", "latex"),
244       "RTF"   : ("NO",  "rtf"),
245       "MAN"   : ("YES", "man"),
246       "XML"   : ("NO",  "xml"),
247    }
248
249    default_file_patterns = (
250       '*.c', '*.cc', '*.cxx', '*.cpp', '*.c++', '*.java', '*.ii', '*.ixx',
251       '*.ipp', '*.i++', '*.inl', '*.h', '*.hh ', '*.hxx', '*.hpp', '*.h++',
252       '*.idl', '*.odl', '*.cs', '*.php', '*.php3', '*.inc', '*.m', '*.mm',
253       '*.py',
254    )
255
256    default_exclude_patterns = (
257       '*~',
258    )
259
260    sources          = []
261    basedir          = node.dir.abspath
262    data             = DoxyfileParse(env, node)
263    recursive        = data.get("RECURSIVE", "NO").upper()=="YES"
264    file_patterns    = data.get("FILE_PATTERNS", default_file_patterns)
265    exclude_patterns = data.get("EXCLUDE_PATTERNS", default_exclude_patterns)
266
267    for i in data.get("INPUT", [ "." ]):
268       input = os.path.normpath(os.path.join(basedir,i))
269       if os.path.isfile(input):
270          sources.append(input)
271       elif os.path.isdir(input):
272          if recursive : entries = os.walk(input)
273          else         : entries = [ (input, [], os.listdir(input)) ]
274          for root, dirs, files in entries:
275             for f in files:
276                filename = os.path.normpath(os.path.join(root, f))
277                if (         reduce(lambda x, y: x or fnmatch(f, y), file_patterns,    False)
278                     and not reduce(lambda x, y: x or fnmatch(f, y), exclude_patterns, False) ):
279                   sources.append(filename)
280
281    for fmt, key in dep_add_keys:
282       if data.has_key(key) and \
283              (fmt is None or data.get("GENERATE_%s" % fmt, output_formats[fmt][0]).upper() == "YES"):
284          elt = env.Flatten(env.subst_list(data[key]))
285          sources.extend([ os.path.normpath(os.path.join(basedir,f))
286                           for f in elt if f ])
287
288    sources = map( lambda path: env.File(path), sources )
289    return sources
290
291 def DoxySourceScanCheck(node, env):
292    """Check if we should scan this file"""
293    return os.path.isfile(node.path) and 'doxyfile' in node.name.lower()
294
295 def DoxyEmitter(source, target, env):
296    """Doxygen Doxyfile emitter"""
297    # possible output formats and their default values and output locations
298    output_formats = {
299       "HTML"  : ("YES", "html"),
300       "LATEX" : ("YES", "latex"),
301       "RTF"   : ("NO",  "rtf"),
302       "MAN"   : ("YES", "man"),
303       "XML"   : ("NO",  "xml"),
304    }
305
306    data = DoxyfileParse(env, source[0])
307
308    targets = []
309    if data.get("OUTPUT_DIRECTORY",""):
310       out_dir = data["OUTPUT_DIRECTORY"]
311       dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir) )
312       dir.sources = source
313       if env.GetOption('clean'):
314          targets.append(dir)
315          return (targets, source)
316    else:
317       out_dir = '.'
318
319    # add our output locations
320    html_dir = None
321    for (k, v) in output_formats.iteritems():
322       if data.get("GENERATE_" + k, v[0]).upper() == "YES" and data.get(k + "_OUTPUT", v[1]):
323          dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir, data.get(k + "_OUTPUT", v[1])) )
324          if k == "HTML" : html_dir = dir
325          dir.sources = source
326          node = env.File( os.path.join(dir.abspath, k.lower()+".stamp" ) )
327          targets.append(node)
328          if env.GetOption('clean'): targets.append(dir)
329
330    if data.get("GENERATE_TAGFILE",""):
331       targets.append(env.File( os.path.join(source[0].dir.abspath, data["GENERATE_TAGFILE"]) ))
332
333    if data.get("SEARCHENGINE","NO").upper() == "YES" and html_dir:
334       targets.append(env.File( os.path.join(html_dir.abspath, "search.idx") ))
335
336    # don't clobber targets
337    for node in targets:
338       env.Precious(node)
339
340    return (targets, source)
341
342 def doxyNodeHtmlDir(env,node):
343    if not node.sources : return None
344    data = DoxyfileParse(env, node.sources[0])
345    if data.get("GENERATE_HTML",'YES').upper() != 'YES' : return None
346    return os.path.normpath(os.path.join( node.sources[0].dir.abspath,
347                                          data.get("OUTPUT_DIRECTORY","."),
348                                          data.get("HTML_OUTPUT","html") ))
349
350 def relpath(source, target):
351    source = os.path.normpath(source)
352    target = os.path.normpath(target)
353    prefix = os.path.dirname(os.path.commonprefix((source,target)))
354    prefix_len = prefix and len(prefix.split(os.sep)) or 0
355    source_elts = source.split(os.sep)
356    target_elts = target.split(os.sep)
357    if source_elts[0] == '..' or target_elts[0] == '..':
358       raise ValueError, "invalid relapth args"
359    return os.path.join(*([".."] * (len(source_elts) - prefix_len) +
360                          target_elts[prefix_len:]))
361
362 def doxyAction(target, source, env):
363    e = {}
364    e.update(env['ENV'])
365    for k,v in env.get('DOXYENV',[]).iteritems() : e[k] = env.subst(v)
366    SCons.Action.Action("$DOXYGENCOM")(target, source, env.Clone(ENV = e), show=False)
367
368 def doxyActionStr(target, source, env):
369    return env.subst("$DOXYGENCOM",target=target,source=source)
370
371 def generate(env):
372    """
373    Add builders and construction variables for the
374    Doxygen tool. This is currently for Doxygen 1.4.6.
375    """
376    doxyfile_scanner = env.Scanner(
377       DoxySourceScan,
378       "DoxySourceScan",
379       scan_check = DoxySourceScanCheck,
380    )
381
382    doxyfile_builder = env.Builder(
383       action = [ SCons.Action.Action(doxyAction, doxyActionStr),
384                  SCons.Action.Action([ "touch $TARGETS" ]) ],
385       emitter = DoxyEmitter,
386       target_factory = env.fs.Entry,
387       single_source = True,
388       source_scanner = doxyfile_scanner
389    )
390
391    env.Append(BUILDERS = {
392       'Doxygen': doxyfile_builder,
393    })
394
395    env.SetDefault(
396       DOXYGENCOM = 'cd ${SOURCE.dir} && doxygen ${SOURCE.file}'
397    )
398
399 def exists(env):
400    """
401    Make sure doxygen exists.
402    """
403    return env.Detect("doxygen")