3b52795f8efbca15ead05515f79c9fb669df9f73
[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    html_dir = None
259    for (k, v) in output_formats.iteritems():
260       if data.get("GENERATE_" + k, v[0]).upper() == "YES":
261          dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir, data.get(k + "_OUTPUT", v[1])) )
262          if k == "HTML" : html_dir = dir
263          dir.sources = source
264          node = env.File( os.path.join(dir.abspath, k.lower()+".stamp" ) )
265          targets.append(node)
266          if env.GetOption('clean'): targets.append(dir)
267
268    if data.has_key("GENERATE_TAGFILE") and html_dir:
269       targets.append(env.File( os.path.join(source[0].dir.abspath, data["GENERATE_TAGFILE"]) ))
270
271    if data.get("SEARCHENGINE","NO").upper() == "YES":
272       targets.append(env.File( os.path.join(html_dir.abspath, "search.idx") ))
273
274    # don't clobber targets
275    for node in targets:
276       env.Precious(node)
277
278    return (targets, source)
279
280 def doxyNodeHtmlDir(env,node):
281    if not node.sources : return None
282    data = DoxyfileParse(env, node.sources[0].abspath)
283    if data.get("GENERATE_HTML",'YES').upper() != 'YES' : return None
284    return os.path.normpath(os.path.join( node.sources[0].dir.abspath,
285                                          data.get("OUTPUT_DIRECTORY","."),
286                                          data.get("HTML_OUTPUT","html") ))
287
288 def relpath(source, target):
289    source = os.path.normpath(source)
290    target = os.path.normpath(target)
291    prefix = os.path.dirname(os.path.commonprefix((source,target)))
292    prefix_len = prefix and len(prefix.split(os.sep)) or 0
293    source_elts = source.split(os.sep)
294    target_elts = target.split(os.sep)
295    if source_elts[0] == '..' or target_elts[0] == '..':
296       raise ValueError, "invalid relapth args"
297    return os.path.join(*([".."] * (len(source_elts) - prefix_len) +
298                          target_elts[prefix_len:]))
299
300 def DoxyGenerator(source, target, env, for_signature):
301
302    data = DoxyfileParse(env, source[0].abspath)
303
304    actions = [ SCons.Action.Action("cd ${SOURCE.dir}  && TOPDIR=%s ${DOXYGEN} ${SOURCE.file}"
305                                    % (relpath(source[0].dir.abspath, env.Dir('#').abspath),)) ]
306
307    # This will add automatic 'installdox' calls.
308    #
309    # For every referenced tagfile, the generator first checks for the
310    # existence of a construction variable '<name>_DOXY_URL' where
311    # '<name>' is the uppercased name of the tagfile sans extension
312    # (e.g. 'Utils.tag' -> 'UTILS_DOXY_URL'). If this variable exists,
313    # it must contain the url or path to the installed documentation
314    # corresponding to the tag file.
315    #
316    # Is the variable is not found and if a referenced tag file is a
317    # target within this same build, the generator will parse the
318    # 'Doxyfile' from which the tag file is built. It will
319    # automatically create the html directory from the information in
320    # that 'Doxyfile'.
321    #
322    # If for any referenced tagfile no url can be found, 'installdox'
323    # will *not* be called and a warning about the missing url is
324    # generated.
325
326    if data.get('GENERATE_HTML','YES').upper() == "YES":
327       output_dir = os.path.normpath(os.path.join( source[0].dir.abspath,
328                                                   data.get("OUTPUT_DIRECTORY","."),
329                                                   data.get("HTML_OUTPUT","html") ))
330       args = []
331       for tagfile in data.get('TAGFILES',[]):
332          url = env.get(os.path.splitext(os.path.basename(tagfile))[0].upper()+"_DOXY_URL", None)
333          if not url:
334             url = doxyNodeHtmlDir(
335                env,
336                env.File(os.path.normpath(os.path.join(str(source[0].dir), tagfile))))
337             if url : url = relpath(output_dir, url)
338          if not url:
339             print "WARNING:",source[0].abspath, ": missing tagfile url for", tagfile
340             args = None
341          if args is not None and url:
342             args.append("-l %s@%s" % ( os.path.basename(tagfile), url ))
343       if args:
344          actions.append(SCons.Action.Action('cd %s && ./installdox %s' % (output_dir, " ".join(args))))
345
346    actions.append(SCons.Action.Action([ "touch $TARGETS" ]))
347
348    return actions
349
350 def generate(env):
351    """
352    Add builders and construction variables for the
353    Doxygen tool. This is currently for Doxygen 1.4.6.
354    """
355    doxyfile_scanner = env.Scanner(
356       DoxySourceScan,
357       "DoxySourceScan",
358       scan_check = DoxySourceScanCheck,
359    )
360
361    doxyfile_builder = env.Builder(
362       # scons 0.96.93 hang on the next line but I don't know hot to FIX the problem
363       generator = DoxyGenerator,
364       emitter = DoxyEmitter,
365       target_factory = env.fs.Entry,
366       single_source = True,
367       source_scanner =  doxyfile_scanner
368    )
369
370    env.Append(BUILDERS = {
371       'Doxygen': doxyfile_builder,
372    })
373
374    env.AppendUnique(
375       DOXYGEN = 'doxygen',
376    )
377
378 def exists(env):
379    """
380    Make sure doxygen exists.
381    """
382    return env.Detect("doxygen")