Added admin scripts to repository
[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 Institut fuer offene Kommunikationssysteme (FOKUS)
22 # Kompetenzzentrum fuer Satelitenkommunikation (SatCom)
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
97 import os.path
98 import glob, re
99 import SCons.Action
100 from fnmatch import fnmatch
101
102 EnvVar = re.compile(r"\$\(([0-9A-Za-z_-]+)\)")
103
104 def DoxyfileParse(env,file):
105    ENV = {}
106    ENV.update(env.get("ENV",{}))
107    ENV['TOPDIR'] = env.Dir('#').abspath
108    data = DoxyfileParse_(file,{},ENV)
109    for k,v in data.items():
110       if not v : del data[k]
111       elif k in ("INPUT", "FILE_PATTERNS", "EXCLUDE_PATTERNS", "@INCLUDE", "TAGFILES") : continue
112       elif len(v)==1 : data[k] = v[0]
113    return data
114
115 def DoxyfileParse_(file, data, ENV):
116    """
117    Parse a Doxygen source file and return a dictionary of all the values.
118    Values will be strings and lists of strings.
119    """
120    try:
121       dir = os.path.dirname(file)
122
123       import shlex
124       lex = shlex.shlex(instream=open(file), posix=True)
125       lex.wordchars += "*+=./-:@~$()"
126       lex.whitespace = lex.whitespace.replace("\n", "")
127       lex.escape = "\\"
128
129       lineno = lex.lineno
130       token = lex.get_token()
131       key = None
132       last_token = ""
133       key_token = True
134       next_key = False
135       new_data = True
136
137       def append_data(data, key, new_data, token):
138          if new_data or len(data[key]) == 0:
139             data[key].append(token)
140          else:
141             data[key][-1] += token
142
143       while token:
144          if token=='\n':
145             if last_token!='\\':
146                key_token = True
147          elif token=='\\':
148             pass
149          elif key_token:
150             key = token
151             key_token = False
152          else:
153             if token=="+=" or (token=="=" and key=="@INCLUDE"):
154                if not data.has_key(key):
155                   data[key] = []
156             elif token == "=":
157                data[key] = []
158             else:
159                token = EnvVar.sub(lambda m,ENV=ENV: str(ENV.get(m.group(1),"")),token)
160                append_data(data, key, new_data, token)
161                new_data = True
162                if key=='@INCLUDE':
163                   inc = os.path.join(dir,data['@INCLUDE'][-1])
164                   if os.path.exists(inc) :
165                      DoxyfileParse_(inc,data,ENV)
166
167          last_token = token
168          token = lex.get_token()
169
170          if last_token=='\\' and token!='\n':
171             new_data = False
172             append_data(data, key, new_data, '\\')
173
174       return data
175    except:
176       return {}
177
178 def DoxySourceScan(node, env, path):
179    """
180    Doxygen Doxyfile source scanner.  This should scan the Doxygen file and add
181    any files used to generate docs to the list of source files.
182    """
183    dep_add_keys = (
184       '@INCLUDE', 'HTML_HEADER', 'HTML_FOOTER', 'TAGFILES', 'INPUT_FILTER'
185    )
186
187    default_file_patterns = (
188       '*.c', '*.cc', '*.cxx', '*.cpp', '*.c++', '*.java', '*.ii', '*.ixx',
189       '*.ipp', '*.i++', '*.inl', '*.h', '*.hh ', '*.hxx', '*.hpp', '*.h++',
190       '*.idl', '*.odl', '*.cs', '*.php', '*.php3', '*.inc', '*.m', '*.mm',
191       '*.py',
192    )
193
194    default_exclude_patterns = (
195       '*~',
196    )
197
198    sources          = []
199    basedir          = node.dir.abspath
200    data             = DoxyfileParse(env, node.abspath)
201    recursive        = data.get("RECURSIVE", "NO").upper()=="YES"
202    file_patterns    = data.get("FILE_PATTERNS", default_file_patterns)
203    exclude_patterns = data.get("EXCLUDE_PATTERNS", default_exclude_patterns)
204
205    for i in data.get("INPUT", [ "." ]):
206       input = os.path.normpath(os.path.join(basedir,i))
207       if os.path.isfile(input):
208          sources.append(input)
209       elif os.path.isdir(input):
210          if recursive : entries = os.walk(input)
211          else         : entries = [ (input, [], os.listdir(input)) ]
212          for root, dirs, files in entries:
213             for f in files:
214                filename = os.path.normpath(os.path.join(root, f))
215                if ( reduce(lambda x, y: x or fnmatch(f, y),
216                            file_patterns, False)
217                     and not reduce(lambda x, y: x or fnmatch(f, y),
218                                    exclude_patterns, False) ):
219                   sources.append(filename)
220
221    for key in dep_add_keys:
222       if data.has_key(key):
223          elt = data[key]
224          if type(elt) is type ("") : elt = [ elt ]
225          sources.extend([ os.path.normpath(os.path.join(basedir,f))
226                           for f in elt ])
227
228    sources = map( lambda path: env.File(path), sources )
229    return sources
230
231 def DoxySourceScanCheck(node, env):
232    """Check if we should scan this file"""
233    return os.path.isfile(node.path)
234
235 def DoxyEmitter(source, target, env):
236    """Doxygen Doxyfile emitter"""
237    # possible output formats and their default values and output locations
238    output_formats = {
239       "HTML"  : ("YES", "html"),
240       "LATEX" : ("YES", "latex"),
241       "RTF"   : ("NO",  "rtf"),
242       "MAN"   : ("YES", "man"),
243       "XML"   : ("NO",  "xml"),
244    }
245
246    data = DoxyfileParse(env, source[0].abspath)
247
248    targets = []
249    if data.has_key("OUTPUT_DIRECTORY"):
250       out_dir = data["OUTPUT_DIRECTORY"]
251       dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir) )
252       dir.sources = source
253       if env.GetOption('clean'): targets.append(dir)
254    else:
255       out_dir = '.'
256
257    # add our output locations
258    for (k, v) in output_formats.iteritems():
259       if data.get("GENERATE_" + k, v[0]).upper() == "YES":
260          dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir, data.get(k + "_OUTPUT", v[1])) )
261          dir.sources = source
262          node = env.File( os.path.join(dir.abspath, k.lower()+".stamp" ) )
263          targets.append(node)
264          if env.GetOption('clean'): targets.append(dir)
265
266    if data.has_key("GENERATE_TAGFILE"):
267       targets.append(env.File( os.path.join(source[0].dir.abspath, data["GENERATE_TAGFILE"]) ))
268
269    # don't clobber targets
270    for node in targets:
271       env.Precious(node)
272
273    return (targets, source)
274
275 def doxyNodeHtmlDir(env,node):
276    if not node.sources : return None
277    data = DoxyfileParse(env, node.sources[0].abspath)
278    if data.get("GENERATE_HTML",'YES').upper() != 'YES' : return None
279    return os.path.normpath(os.path.join( node.sources[0].dir.abspath,
280                                          data.get("OUTPUT_DIRECTORY","."),
281                                          data.get("HTML_OUTPUT","html") ))
282
283 def relpath(source, target):
284    source = os.path.normpath(source)
285    target = os.path.normpath(target)
286    prefix = os.path.dirname(os.path.commonprefix((source,target)))
287    prefix_len = prefix and len(prefix.split(os.sep)) or 0
288    source_elts = source.split(os.sep)
289    target_elts = target.split(os.sep)
290    if source_elts[0] == '..' or target_elts[0] == '..':
291       raise ValueError, "invalid relapth args"
292    return os.path.join(*([".."] * (len(source_elts) - prefix_len) +
293                          target_elts[prefix_len:]))
294
295 def DoxyGenerator(source, target, env, for_signature):
296
297    data = DoxyfileParse(env, source[0].abspath)
298
299    actions = [ SCons.Action.Action("cd ${SOURCE.dir}  && TOPDIR=%s ${DOXYGEN} ${SOURCE.file}"
300                                    % (relpath(source[0].dir.abspath, env.Dir('#').abspath),)) ]
301
302    # This will add automatic 'installdox' calls.
303    #
304    # For every referenced tagfile, the generator first checks for the
305    # existence of a construction variable '<name>_DOXY_URL' where
306    # '<name>' is the uppercased name of the tagfile sans extension
307    # (e.g. 'Utils.tag' -> 'UTILS_DOXY_URL'). If this variable exists,
308    # it must contain the url or path to the installed documentation
309    # corresponding to the tag file.
310    #
311    # Is the variable is not found and if a referenced tag file is a
312    # target within this same build, the generator will parse the
313    # 'Doxyfile' from which the tag file is built. It will
314    # automatically create the html directory from the information in
315    # that 'Doxyfile'.
316    #
317    # If for any referenced tagfile no url can be found, 'installdox'
318    # will *not* be called and a warning about the missing url is
319    # generated.
320
321    if data.get('GENERATE_HTML','YES').upper() == "YES":
322       output_dir = os.path.normpath(os.path.join( source[0].dir.abspath,
323                                                   data.get("OUTPUT_DIRECTORY","."),
324                                                   data.get("HTML_OUTPUT","html") ))
325       args = []
326       for tagfile in data.get('TAGFILES',[]):
327          url = env.get(os.path.splitext(os.path.basename(tagfile))[0].upper()+"_DOXY_URL", None)
328          if not url:
329             url = doxyNodeHtmlDir(
330                env,
331                env.File(os.path.normpath(os.path.join(str(source[0].dir), tagfile))))
332             if url : url = relpath(output_dir, url)
333          if not url:
334             print "WARNING:",source[0].abspath, ": missing tagfile url for", tagfile
335             args = None
336          if args is not None and url:
337             args.append("-l %s@%s" % ( os.path.basename(tagfile), url ))
338       if args:
339          actions.append(SCons.Action.Action('cd %s && ./installdox %s' % (output_dir, " ".join(args))))
340
341    actions.append(SCons.Action.Action([ "touch $TARGETS" ]))
342
343    return actions
344
345 def generate(env):
346    """
347    Add builders and construction variables for the
348    Doxygen tool. This is currently for Doxygen 1.4.6.
349    """
350    doxyfile_scanner = env.Scanner(
351       DoxySourceScan,
352       "DoxySourceScan",
353       scan_check = DoxySourceScanCheck,
354    )
355
356    doxyfile_builder = env.Builder(
357       # scons 0.96.93 hang on the next line but I don't know hot to FIX the problem
358       generator = DoxyGenerator,
359       emitter = DoxyEmitter,
360       target_factory = env.fs.Entry,
361       single_source = True,
362       source_scanner =  doxyfile_scanner
363    )
364
365    env.Append(BUILDERS = {
366       'Doxygen': doxyfile_builder,
367    })
368
369    env.AppendUnique(
370       DOXYGEN = 'doxygen',
371    )
372
373 def exists(env):
374    """
375    Make sure doxygen exists.
376    """
377    return env.Detect("doxygen")