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