Buildsystem updates
[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, path, env, include_path=None, items = None):
155       self._env = env
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 {}
160
161    def parse(self):
162       while True:
163          var = self._lexer.var()
164          if not var: break;
165          op = self._lexer.op()
166          value = [ self._envsub(v) for v in self._lexer ]
167          if not value:
168             raise ValueError,"Missing value in assignment"
169          if var[0] == '@':
170             self._meta(var,op,value)
171          elif op == '=':
172             self._items[var] = value
173          else:
174             self._items.setdefault(var,[]).extend(value)
175
176    def _envsub(self,value):
177       return self.ENVVAR_RE.sub(lambda m, env=self._env : str(env.get(m.group(1),"")), value)
178
179    def _meta(self, cmd, op, value):
180       m = '_'+cmd[1:]
181       try:
182          m = getattr(self,m)
183       except AttributeError:
184          raise ValueError,'Unknown meta command ' + cmd
185       m(op,value)
186
187    def _INCLUDE(self, op, value):
188       if len(value) != 1:
189          raise ValueError,"Invalid argument to @INCLUDE"
190       
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)
196             parser.parse()
197             return
198
199       raise ValueError,"@INCLUDE file '%s' not found" % value[0]
200
201    def _INCLUDE_PATH(self, op, value):
202       self._include_path.extend(value)
203
204    def items(self):
205       return self._items
206
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():
211       return {}
212    ENV = {}
213    ENV.update(env.get("ENV",{}))
214    ENV.update(env.get("DOXYENV", {}))
215    parser = DoxyfileParser(file,ENV)
216    try:
217       parser.parse()
218    except ValueError, v:
219       print "WARNING: Error while parsing doxygen configuration '%s': %s" % (str(file),str(v))
220       return {}
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]
226    return data
227
228 def DoxySourceScan(node, env, path):
229    """
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.
232    """
233    dep_add_keys = (
234       ('HTML', 'LAYOUT_FILE'), 
235       (None,   '@INCLUDE'), 
236       ('HTML', 'HTML_HEADER'), 
237       ('HTML', 'HTML_FOOTER'), 
238       (None,   'TAGFILES'), 
239       (None,   'INPUT_FILTER'),
240    )
241
242    output_formats = {
243       "HTML"  : ("YES", "html"),
244       "LATEX" : ("YES", "latex"),
245       "RTF"   : ("NO",  "rtf"),
246       "MAN"   : ("YES", "man"),
247       "XML"   : ("NO",  "xml"),
248    }
249
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',
254       '*.py',
255    )
256
257    default_exclude_patterns = (
258       '*~',
259    )
260
261    sources          = []
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)
267
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:
276             for f in files:
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)
281
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))
287                           for f in elt if f ])
288
289    sources = map( lambda path: env.File(path), sources )
290    return sources
291
292 def DoxySourceScanCheck(node, env):
293    """Check if we should scan this file"""
294    return os.path.isfile(node.path)
295
296 def DoxyEmitter(source, target, env):
297    """Doxygen Doxyfile emitter"""
298    # possible output formats and their default values and output locations
299    output_formats = {
300       "HTML"  : ("YES", "html"),
301       "LATEX" : ("YES", "latex"),
302       "RTF"   : ("NO",  "rtf"),
303       "MAN"   : ("YES", "man"),
304       "XML"   : ("NO",  "xml"),
305    }
306
307    data = DoxyfileParse(env, source[0].abspath)
308
309    targets = []
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) )
313       dir.sources = source
314       if env.GetOption('clean'):
315          targets.append(dir)
316          return (targets, source)
317    else:
318       out_dir = '.'
319
320    # add our output locations
321    html_dir = None
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
326          dir.sources = source
327          node = env.File( os.path.join(dir.abspath, k.lower()+".stamp" ) )
328          targets.append(node)
329          if env.GetOption('clean'): targets.append(dir)
330
331    if data.get("GENERATE_TAGFILE",""):
332       targets.append(env.File( os.path.join(source[0].dir.abspath, data["GENERATE_TAGFILE"]) ))
333
334    if data.get("SEARCHENGINE","NO").upper() == "YES" and html_dir:
335       targets.append(env.File( os.path.join(html_dir.abspath, "search.idx") ))
336
337    # don't clobber targets
338    for node in targets:
339       env.Precious(node)
340
341    return (targets, source)
342
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") ))
350
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:]))
362
363 def doxyAction(target, source, env):
364    e = {}
365    e.update(env['ENV'])
366    for k,v in env.get('DOXYENV',[]).iteritems() : e[k] = env.subst(v)
367    SCons.Action.Action("$DOXYGENCOM")(target, source, env.Clone(ENV = e), show=False)
368
369 def doxyActionStr(target, source, env):
370    return env.subst("$DOXYGENCOM",target=target,source=source)
371
372 def generate(env):
373    """
374    Add builders and construction variables for the
375    Doxygen tool. This is currently for Doxygen 1.4.6.
376    """
377    doxyfile_scanner = env.Scanner(
378       DoxySourceScan,
379       "DoxySourceScan",
380       scan_check = DoxySourceScanCheck,
381    )
382
383    doxyfile_builder = env.Builder(
384       action = [ SCons.Action.Action(doxyAction, doxyActionStr),
385                  SCons.Action.Action([ "touch $TARGETS" ]) ],
386       emitter = DoxyEmitter,
387       target_factory = env.fs.Entry,
388       single_source = True,
389       source_scanner = doxyfile_scanner
390    )
391
392    env.Append(BUILDERS = {
393       'Doxygen': doxyfile_builder,
394    })
395
396    env.SetDefault(
397       DOXYGENCOM = 'cd ${SOURCE.dir} && doxygen ${SOURCE.file}'
398    )
399
400 def exists(env):
401    """
402    Make sure doxygen exists.
403    """
404    return env.Detect("doxygen")