Again some doc-build fixes
[senf.git] / senfscons / Doxygen.py
1 # Astxx, the Asterisk C++ API and Utility Library.
2 # Copyright (C) 2005, 2006  Matthew A. Nicholson
3 # Copyright (C) 2006  Tim Blechmann
4 #
5 # This library is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU Lesser General Public
7 # License version 2.1 as published by the Free Software Foundation.
8 #
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 # Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
18 # I have been fighting 4 problems in this implementation:
19 # - A Directory target will *not* call any source scanners
20 # - A Directory target will interpret the directory contents as
21 #   sources not targets. This means, that if a command creates that
22 #   directory plus contents, the target will never be up-to-date
23 #   (since the directory contents will change with every call of
24 #   scons)
25 # - Theres a bug in SCons which will produce an error message for
26 #   directory targets if dir.sources is not set explicitly
27 # - the first argument to env.Clean() must be the command line target,
28 #   with which the scons was invoked. This does not help to add
29 #   aditional files or directories to be cleaned if you don't know
30 #   that target (it's not possible to say 'if you clean this file,
31 #   also clean that one' hich is, what I had expected env.Clean to
32 #   do).
33 #
34 # Together, these problems have produced several difficulties. I have
35 # solved them by
36 # - Adding an (empty) stamp file as a (file) target. This target will
37 #   cause source scanners to be invoked
38 # - Adding the documentation directory as a target (so it will be
39 #   cleaned up which env.Clean doesn't help me to do), but *only* if
40 #   scons is called to with the -c option
41 # - Setting dir.sources to the known source-list to silence the error
42 #   message whenever a directory is added as a target
43 #
44 # You will find all this in the DoxyEmitter
45
46 import os, sys, traceback
47 import os.path
48 import glob
49 from fnmatch import fnmatch
50
51 def DoxyfileParse(file):
52    data = DoxyfileParse_(file,{})
53    for (k,v) in data.items():
54       if not v : del data[k]
55       elif k in ("INPUT", "FILE_PATTERNS", "EXCLUDE_PATTERNS", "@INCLUDE", "TAGFILES") : continue
56       elif len(v)==1 : data[k] = v[0]
57    return data
58
59 def DoxyfileParse_(file, data):
60    """
61    Parse a Doxygen source file and return a dictionary of all the values.
62    Values will be strings and lists of strings.
63    """
64    try:
65       dir = os.path.dirname(file)
66
67       import shlex
68       lex = shlex.shlex(instream=open(file), posix=True)
69       lex.wordchars += "*+./-:@~"
70       lex.whitespace = lex.whitespace.replace("\n", "")
71       lex.escape = "\\"
72
73       lineno = lex.lineno
74       token = lex.get_token()
75       key = None
76       last_token = ""
77       key_token = True
78       next_key = False
79       new_data = True
80
81       def append_data(data, key, new_data, token):
82          if new_data or len(data[key]) == 0:
83             data[key].append(token)
84          else:
85             data[key][-1] += token
86
87       while token:
88          if token=='\n':
89             if last_token!='\\':
90                key_token = True
91          elif token=='\\':
92             pass
93          elif key_token:
94             key = token
95             key_token = False
96          else:
97             if token=="+=" or (token=="=" and key=="@INCLUDE"):
98                if not data.has_key(key):
99                   data[key] = []
100             elif token == "=":
101                data[key] = []
102             else:
103                append_data(data, key, new_data, token)
104                new_data = True
105                if key=='@INCLUDE':
106                   inc = os.path.join(dir,data['@INCLUDE'][-1])
107                   if os.path.exists(inc) :
108                      DoxyfileParse_(inc,data)
109
110          last_token = token
111          token = lex.get_token()
112
113          if last_token=='\\' and token!='\n':
114             new_data = False
115             append_data(data, key, new_data, '\\')
116
117       return data
118    except:
119       return {}
120
121 def DoxySourceScan(node, env, path):
122    """
123    Doxygen Doxyfile source scanner.  This should scan the Doxygen file and add
124    any files used to generate docs to the list of source files.
125    """
126    dep_add_keys = (
127       '@INCLUDE', 'HTML_HEADER', 'HTML_FOOTER', 'TAGFILES'
128    )
129    
130    default_file_patterns = (
131       '*.c', '*.cc', '*.cxx', '*.cpp', '*.c++', '*.java', '*.ii', '*.ixx',
132       '*.ipp', '*.i++', '*.inl', '*.h', '*.hh ', '*.hxx', '*.hpp', '*.h++',
133       '*.idl', '*.odl', '*.cs', '*.php', '*.php3', '*.inc', '*.m', '*.mm',
134       '*.py',
135    )
136
137    default_exclude_patterns = (
138       '*~',
139    )
140
141    sources          = []
142    basedir          = node.dir.abspath
143    data             = DoxyfileParse(node.abspath)
144    recursive        = data.get("RECURSIVE", "NO").upper()=="YES"
145    file_patterns    = data.get("FILE_PATTERNS", default_file_patterns)
146    exclude_patterns = data.get("EXCLUDE_PATTERNS", default_exclude_patterns)
147
148    for i in data.get("INPUT", [ "." ]):
149       input = os.path.normpath(os.path.join(basedir,i))
150       if os.path.isfile(input):
151          sources.append(input)
152       elif os.path.isdir(input):
153          if recursive : entries = os.walk(input)
154          else         : entries = [ (input, [], os.listdir(input)) ]
155          for root, dirs, files in entries:
156             for f in files:
157                filename = os.path.normpath(os.path.join(root, f))
158                if ( reduce(lambda x, y: x or fnmatch(f, y),
159                            file_patterns, False) 
160                     and not reduce(lambda x, y: x or fnmatch(f, y),
161                                    exclude_patterns, False) ):
162                   sources.append(filename)
163
164    for key in dep_add_keys:
165       if data.has_key(key):
166          elt = data[key]
167          if type(elt) is type ("") : elt = [ elt ]
168          sources.extend([ os.path.normpath(os.path.join(basedir,f))
169                           for f in elt ])
170
171    sources = map( lambda path: env.File(path), sources )
172    return sources
173
174 def DoxySourceScanCheck(node, env):
175    """Check if we should scan this file"""
176    return os.path.isfile(node.path)
177
178 def DoxyEmitter(source, target, env):
179    """Doxygen Doxyfile emitter"""
180    # possible output formats and their default values and output locations
181    output_formats = {
182       "HTML"  : ("YES", "html"),
183       "LATEX" : ("YES", "latex"),
184       "RTF"   : ("NO",  "rtf"),
185       "MAN"   : ("YES", "man"),
186       "XML"   : ("NO",  "xml"),
187    }
188
189    data = DoxyfileParse(source[0].abspath)
190
191    targets = []
192    if data.has_key("OUTPUT_DIRECTORY"):
193       out_dir = data["OUTPUT_DIRECTORY"]
194       dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir) )
195       dir.sources = source
196       if env.GetOption('clean'): targets.append(dir)
197    else:
198       out_dir = '.'
199
200    # add our output locations
201    for (k, v) in output_formats.iteritems():
202       if data.get("GENERATE_" + k, v[0]).upper() == "YES":
203          dir = env.Dir( os.path.join(source[0].dir.abspath, out_dir, data.get(k + "_OUTPUT", v[1])) )
204          dir.sources = source
205          node = env.File( os.path.join(dir.abspath, k.lower()+".stamp" ) )
206          targets.append(node)
207          if env.GetOption('clean'): targets.append(dir)
208
209    if data.has_key("GENERATE_TAGFILE"):
210       targets.append(env.File( os.path.join(source[0].dir.abspath, data["GENERATE_TAGFILE"]) ))
211
212    # don't clobber targets
213    for node in targets:
214       env.Precious(node)
215
216    return (targets, source)
217
218 def doxyNodeHtmlDir(node):
219    if not node.sources : return None
220    data = DoxyfileParse(node.sources[0].abspath)
221    if data.get("GENERATE_HTML",'YES').upper() != 'YES' : return None
222    return os.path.normpath(os.path.join( node.sources[0].dir.abspath,
223                                          data.get("OUTPUT_DIRECTORY","."),
224                                          data.get("HTML_OUTPUT","html") ))
225
226 def relpath(source, target):
227    source = os.path.normpath(source)
228    target = os.path.normpath(target)
229    prefix = os.path.dirname(os.path.commonprefix((source,target)))
230    prefix_len = prefix and len(prefix.split(os.sep)) or 0
231    source_elts = source.split(os.sep)
232    target_elts = target.split(os.sep)
233    if source_elts[0] == '..' or target_elts[0] == '..':
234       raise ValueError, "invalid relapth args"
235    return os.path.join(*([".."] * (len(source_elts) - prefix_len) +
236                          target_elts[prefix_len:]))
237
238 def DoxyGenerator(source, target, env, for_signature):
239
240    data = DoxyfileParse(source[0].abspath)
241
242    actions = [ env.Action("cd ${SOURCE.dir}  &&  ${DOXYGEN} ${SOURCE.file}") ]
243
244    # This will add automatic 'installdox' calls.
245    #
246    # For every referenced tagfile, the generator first checks for the
247    # existence of a construction variable '<name>_DOXY_URL' where
248    # '<name>' is the uppercased name of the tagfile sans extension
249    # (e.g. 'Utils.tag' -> 'UTILS_DOXY_URL'). If this variable exists,
250    # it must contain the url or path to the installed documentation
251    # corresponding to the tag file.
252    #
253    # Is the variable is not found and if a referenced tag file is a
254    # target within this same build, the generator will parse the
255    # 'Doxyfile' from which the tag file is built. It will
256    # automatically create the html directory from the information in
257    # that 'Doxyfile'.
258    #
259    # If for any referenced tagfile no url can be found, 'installdox'
260    # will *not* be called and a warning about the missing url is
261    # generated.
262    
263    if data.get('GENERATE_HTML','YES').upper() == "YES":
264       output_dir = os.path.normpath(os.path.join( source[0].dir.abspath,
265                                                   data.get("OUTPUT_DIRECTORY","."),
266                                                   data.get("HTML_OUTPUT","html") ))
267       args = []
268       for tagfile in data.get('TAGFILES',[]):
269          url = env.get(os.path.splitext(os.path.basename(tagfile))[0].upper()+"_DOXY_URL", None)
270          if not url:
271             url = doxyNodeHtmlDir(
272                env.File(os.path.normpath(os.path.join(str(source[0].dir), tagfile))))
273             if url : url = relpath(output_dir, url)
274          if not url:
275             print "WARNING:",source[0].abspath, ": missing tagfile url for", tagfile
276             args = None
277          if args is not None and url:
278             args.append("-l %s@%s" % ( os.path.basename(tagfile), url ))
279       if args:
280          actions.append(env.Action('cd %s && ./installdox %s' % (output_dir, " ".join(args))))
281    
282    actions.append(env.Action([ "touch $TARGETS" ]))
283
284    return actions
285
286 def generate(env):
287    """
288    Add builders and construction variables for the
289    Doxygen tool.  This is currently for Doxygen 1.4.6.
290    """
291    doxyfile_scanner = env.Scanner(
292       DoxySourceScan,
293       "DoxySourceScan",
294       scan_check = DoxySourceScanCheck,
295    )
296
297    doxyfile_builder = env.Builder(
298       # scons 0.96.93 hang on the next line but I don't know hot to FIX the problem
299       generator = DoxyGenerator,
300       emitter = DoxyEmitter,
301       target_factory = env.fs.Entry,
302       single_source = True,
303       source_scanner =  doxyfile_scanner
304    )
305
306    env.Append(BUILDERS = {
307       'Doxygen': doxyfile_builder,
308    })
309
310    env.AppendUnique(
311       DOXYGEN = 'doxygen',
312    )
313
314 def exists(env):
315    """
316    Make sure doxygen exists.
317    """
318    return env.Detect("doxygen")