Complete SENFSCons documentation
[senf.git] / senfscons / Doxygen.py
index f379f91..1d51a69 100644 (file)
@@ -1,3 +1,5 @@
+# The Doxygen builder is based on the Doxygen builder from:
+#
 # Astxx, the Asterisk C++ API and Utility Library.
 # Copyright (C) 2005, 2006  Matthew A. Nicholson
 # Copyright (C) 2006  Tim Blechmann
 # License along with this library; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+# The Modifications are Copyright (C) 2006,2007
+# Fraunhofer Institut fuer offene Kommunikationssysteme (FOKUS)
+# Kompetenzzentrum fuer Satelitenkommunikation (SatCom)
+#     Stefan Bund <g0dil@berlios.de>
+
+## \file
+# \brief Doxygen builder
+
+## \package senfscons.Doxygen
+# \brief Doxygen Documentation Builder
+#
+# This builder will invoke \c doxygen to build software
+# documentation. The doxygen builder only takes the name of the
+# doxyfile as it's source file. The builder parses that doxygen
+# configuration file.
+#
+# The builder will automatically find all sources on which the
+# documentation depends. This includes
+# \li the source code files (as selected by the \c RECURSIVE, \c
+#     FILE_PATTERNS, \c INPUT and \c EXCLUDE_PATTERNS doxygen
+#     directives
+# \li the \c HTML_HEADER and \c HTML_FOOTER
+# \li all referenced \c TAGFILES
+# \li the \c INPUT_FILTER
+# \li all included doxyfiles (via \c @INCLUDE)
+#
+# The builder will emit a list of targets built by doxygen. This
+# depends on the types of documentation built.
+#
+# The builder will also generate additional commands to resolve
+# cross-references to other module documentations. This is based on
+# the \c TAGFILES used. Tagfiles built in the same project in other
+# modules are automatically found and the links will be resolved
+# correctly. To resolve links from external tagfiles, you may specify
+# <i>tagfilename</i><tt>_DOXY_URL</tt> as a construction environment
+# variable to specify the path to resolve references from the given
+# tagfile to. <i>tagfilename</i> is the uppercased basename of the
+# tagfile used.
+#
+# \par Construction Envrionment Variables:
+# <table class="senf">
+# <tr><td>\c DOXYGEN</td><td>doxygen command, defaults to \c doxygen</td></tr>
+# <tr><td><i>tag</i><tt>_DOXY_URL</tt></td><td>external tagfile resolve URL</td></tr>
+# </table>
+#
+# \ingroup builder
+
+# I (g0dil@berlios.de) have been fighting 4 problems in this
+# implementation:
+# - A Directory target will *not* call any source scanners
+# - A Directory target will interpret the directory contents as
+#   sources not targets. This means, that if a command creates that
+#   directory plus contents, the target will never be up-to-date
+#   (since the directory contents will change with every call of
+#   scons)
+# - Theres a bug in SCons which will produce an error message for
+#   directory targets if dir.sources is not set explicitly
+# - the first argument to env.Clean() must be the command line target,
+#   with which the scons was invoked. This does not help to add
+#   aditional files or directories to be cleaned if you don't know
+#   that target (it's not possible to say 'if you clean this file,
+#   also clean that one' hich is, what I had expected env.Clean to
+#   do).
+#
+# Together, these problems have produced several difficulties. I have
+# solved them by
+# - Adding an (empty) stamp file as a (file) target. This target will
+#   cause source scanners to be invoked
+# - Adding the documentation directory as a target (so it will be
+#   cleaned up which env.Clean doesn't help me to do), but *only* if
+#   scons is called to with the -c option
+# - Setting dir.sources to the known source-list to silence the error
+#   message whenever a directory is added as a target
+#
+# You will find all this in the DoxyEmitter
+
 import os, sys, traceback
 import os.path
-import glob
+import glob, re
 from fnmatch import fnmatch
 
-def DoxyfileParse(file):
-   data = DoxyfileParse_(file,{})
-   for (k,v) in data.items():
+EnvVar = re.compile(r"\$\(([0-9A-Za-z_-]+)\)")
+
+def DoxyfileParse(env,file):
+   ENV = {}
+   ENV.update(env.get("ENV",{}))
+   ENV['TOPDIR'] = env.Dir('#').abspath
+   data = DoxyfileParse_(file,{},ENV)
+   for k,v in data.items():
       if not v : del data[k]
       elif k in ("INPUT", "FILE_PATTERNS", "EXCLUDE_PATTERNS", "@INCLUDE", "TAGFILES") : continue
       elif len(v)==1 : data[k] = v[0]
    return data
 
-def DoxyfileParse_(file, data):
+def DoxyfileParse_(file, data, ENV):
    """
    Parse a Doxygen source file and return a dictionary of all the values.
    Values will be strings and lists of strings.
@@ -38,9 +121,9 @@ def DoxyfileParse_(file, data):
 
       import shlex
       lex = shlex.shlex(instream=open(file), posix=True)
-      lex.wordchars += "*+./-:@~"
+      lex.wordchars += "*+=./-:@~$()"
       lex.whitespace = lex.whitespace.replace("\n", "")
-      lex.escape = ""
+      lex.escape = "\\"
 
       lineno = lex.lineno
       token = lex.get_token()
@@ -72,12 +155,13 @@ def DoxyfileParse_(file, data):
             elif token == "=":
                data[key] = []
             else:
+               token = EnvVar.sub(lambda m,ENV=ENV: str(ENV.get(m.group(1),"")),token)
                append_data(data, key, new_data, token)
                new_data = True
                if key=='@INCLUDE':
                   inc = os.path.join(dir,data['@INCLUDE'][-1])
                   if os.path.exists(inc) :
-                     DoxyfileParse_(inc,data)
+                     DoxyfileParse_(inc,data,ENV)
 
          last_token = token
          token = lex.get_token()
@@ -96,7 +180,7 @@ def DoxySourceScan(node, env, path):
    any files used to generate docs to the list of source files.
    """
    dep_add_keys = (
-      '@INCLUDE', 'HTML_HEADER', 'HTML_FOOTER', 'TAGFILES'
+      '@INCLUDE', 'HTML_HEADER', 'HTML_FOOTER', 'TAGFILES', 'INPUT_FILTER'
    )
    
    default_file_patterns = (
@@ -112,7 +196,7 @@ def DoxySourceScan(node, env, path):
 
    sources          = []
    basedir          = node.dir.abspath
-   data             = DoxyfileParse(node.abspath)
+   data             = DoxyfileParse(env, node.abspath)
    recursive        = data.get("RECURSIVE", "NO").upper()=="YES"
    file_patterns    = data.get("FILE_PATTERNS", default_file_patterns)
    exclude_patterns = data.get("EXCLUDE_PATTERNS", default_exclude_patterns)
@@ -158,24 +242,28 @@ def DoxyEmitter(source, target, env):
       "XML"   : ("NO",  "xml"),
    }
 
-   data = DoxyfileParse(source[0].abspath)
+   data = DoxyfileParse(env, source[0].abspath)
 
    targets = []
-   out_dir = data.get("OUTPUT_DIRECTORY", ".")
+   if data.has_key("OUTPUT_DIRECTORY"):
+      out_dir = data["OUTPUT_DIRECTORY"]
+      dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir) )
+      dir.sources = source
+      if env.GetOption('clean'): targets.append(dir)
+   else:
+      out_dir = '.'
 
    # add our output locations
    for (k, v) in output_formats.iteritems():
       if data.get("GENERATE_" + k, v[0]).upper() == "YES":
-         # Grmpf ... need to use a File object here. The problem is, that
-         # Dir.scan() is implemented to just return the directory entries
-         # and does *not* invoke the source-file scanners .. ARGH !!
-         dir = env.Dir( os.path.join(str(source[0].dir), out_dir, data.get(k + "_OUTPUT", v[1])) )
-         node = env.File( os.path.join(str(dir), ".stamp" ) )
-         env.Clean(node, dir)
-         targets.append( node )
+         dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir, data.get(k + "_OUTPUT", v[1])) )
+         dir.sources = source
+         node = env.File( os.path.join(dir.abspath, k.lower()+".stamp" ) )
+         targets.append(node)
+         if env.GetOption('clean'): targets.append(dir)
 
    if data.has_key("GENERATE_TAGFILE"):
-      targets.append(env.File( os.path.join(str(source[0].dir), data["GENERATE_TAGFILE"]) ))
+      targets.append(env.File( os.path.join(source[0].dir.abspath, data["GENERATE_TAGFILE"]) ))
 
    # don't clobber targets
    for node in targets:
@@ -183,9 +271,9 @@ def DoxyEmitter(source, target, env):
 
    return (targets, source)
 
-def doxyNodeHtmlDir(node):
+def doxyNodeHtmlDir(env,node):
    if not node.sources : return None
-   data = DoxyfileParse(node.sources[0].abspath)
+   data = DoxyfileParse(env, node.sources[0].abspath)
    if data.get("GENERATE_HTML",'YES').upper() != 'YES' : return None
    return os.path.normpath(os.path.join( node.sources[0].dir.abspath,
                                          data.get("OUTPUT_DIRECTORY","."),
@@ -205,9 +293,10 @@ def relpath(source, target):
 
 def DoxyGenerator(source, target, env, for_signature):
 
-   data = DoxyfileParse(source[0].abspath)
+   data = DoxyfileParse(env, source[0].abspath)
 
-   actions = [ env.Action("cd ${SOURCE.dir}  &&  ${DOXYGEN} ${SOURCE.file}") ]
+   actions = [ env.Action("cd ${SOURCE.dir}  && TOPDIR=%s ${DOXYGEN} ${SOURCE.file}"
+                          % (relpath(source[0].dir.abspath, env.Dir('#').abspath),)) ]
 
    # This will add automatic 'installdox' calls.
    #
@@ -237,6 +326,7 @@ def DoxyGenerator(source, target, env, for_signature):
          url = env.get(os.path.splitext(os.path.basename(tagfile))[0].upper()+"_DOXY_URL", None)
          if not url:
             url = doxyNodeHtmlDir(
+               env,
                env.File(os.path.normpath(os.path.join(str(source[0].dir), tagfile))))
             if url : url = relpath(output_dir, url)
          if not url: