Extended doxyfile builder to invoke installdox
[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 import os, sys, traceback
19 import os.path
20 import glob
21 from fnmatch import fnmatch
22
23 def DoxyfileParse(file_contents, dir, data = None):
24    """
25    Parse a Doxygen source file and return a dictionary of all the values.
26    Values will be strings and lists of strings.
27    """
28    try:
29       if data is None : data = {}
30
31       import shlex
32       lex = shlex.shlex(instream = file_contents, posix = True)
33       lex.wordchars += "*+./-:"
34       lex.whitespace = lex.whitespace.replace("\n", "")
35       lex.escape = ""
36
37       lineno = lex.lineno
38       token = lex.get_token()
39       key = None
40       last_token = ""
41       key_token = True
42       next_key = False
43       new_data = True
44
45       def append_data(data, key, new_data, token):
46          if new_data or len(data[key]) == 0:
47             data[key].append(token)
48          else:
49             data[key][-1] += token
50
51       while token:
52          if token in ['\n']:
53             if last_token not in ['\\']:
54                key_token = True
55          elif token in ['\\']:
56             pass
57          elif key_token:
58             if key == '@' : key += token
59             else          : key = token
60             if token != '@' : key_token = False
61          else:
62             if token == "+=" or (token == "=" and key == "@INCLUDE"):
63                if not data.has_key(key):
64                   data[key] = list()
65             elif token == "=":
66                data[key] = list()
67             else:
68                append_data( data, key, new_data, token )
69                new_data = True
70                if key == '@INCLUDE':
71                   inc = os.path.join(dir,data['@INCLUDE'][-1])
72                   if os.path.exists(inc) :
73                      DoxyfileParse(open(inc).read(),dir,data)
74
75          last_token = token
76          token = lex.get_token()
77
78          if last_token == '\\' and token != '\n':
79             new_data = False
80             append_data( data, key, new_data, '\\' )
81
82       # compress lists of len 1 into single strings
83       for (k, v) in data.items():
84          if len(v) == 0:
85             data.pop(k)
86
87          # items in the following list will be kept as lists and not converted to strings
88          if k in ["INPUT", "FILE_PATTERNS", "EXCLUDE_PATTERNS", "@INCLUDE", "TAGFILES"]:
89             continue
90
91          if len(v) == 1:
92             data[k] = v[0]
93
94
95       return data
96    except:
97       return {}
98
99 def DoxySourceScan(node, env, path):
100    """
101    Doxygen Doxyfile source scanner.  This should scan the Doxygen file and add
102    any files used to generate docs to the list of source files.
103    """
104    dep_add_keys = [
105       '@INCLUDE', 'HTML_HEADER', 'HTML_FOOTER', 'TAGFILES'
106    ]
107    
108    default_file_patterns = [
109       '*.c', '*.cc', '*.cxx', '*.cpp', '*.c++', '*.java', '*.ii', '*.ixx',
110       '*.ipp', '*.i++', '*.inl', '*.h', '*.hh ', '*.hxx', '*.hpp', '*.h++',
111       '*.idl', '*.odl', '*.cs', '*.php', '*.php3', '*.inc', '*.m', '*.mm',
112       '*.py',
113    ]
114
115    default_exclude_patterns = [
116       '*~',
117    ]
118
119    sources          = []
120    basedir          = str(node.dir)
121    data             = DoxyfileParse(node.get_contents(), basedir)
122    recursive        = ( data.get("RECURSIVE", "NO") == "YES" )
123    file_patterns    = data.get("FILE_PATTERNS", default_file_patterns)
124    exclude_patterns = data.get("EXCLUDE_PATTERNS", default_exclude_patterns)
125
126    for i in data.get("INPUT", [ "." ]):
127       input = os.path.normpath(os.path.join(basedir,i))
128       if os.path.isfile(input):
129          sources.append(input)
130       elif os.path.isdir(input):
131          if recursive : entries = os.walk(input)
132          else         : entries = [ (input, [], os.listdir(input)) ]
133          for root, dirs, files in entries:
134             for f in files:
135                filename = os.path.normpath(os.path.join(root, f))
136                if ( reduce(lambda x, y: x or fnmatch(filename, y),
137                            file_patterns, False) 
138                     and not reduce(lambda x, y: x or fnmatch(filename, y),
139                                    exclude_patterns, False) ):
140                   sources.append(filename)
141
142    for key in dep_add_keys:
143       if data.has_key(key):
144          elt = data[key]
145          if type(elt) is type ("") : elt = [ elt ]
146          sources.extend([ os.path.normpath(os.path.join(basedir,f))
147                           for f in elt ])
148
149    sources = map( lambda path: env.File(path), sources )
150    return sources
151
152 def DoxySourceScanCheck(node, env):
153    """Check if we should scan this file"""
154    return os.path.isfile(node.path)
155
156 def DoxyEmitter(source, target, env):
157    """Doxygen Doxyfile emitter"""
158    # possible output formats and their default values and output locations
159    output_formats = {
160       "HTML": ("YES", "html"),
161       "LATEX": ("YES", "latex"),
162       "RTF": ("NO", "rtf"),
163       "MAN": ("YES", "man"),
164       "XML": ("NO", "xml"),
165    }
166
167    data = DoxyfileParse(source[0].get_contents(), str(source[0].dir))
168
169    targets = []
170    out_dir = data.get("OUTPUT_DIRECTORY", ".")
171
172    # add our output locations
173    for (k, v) in output_formats.items():
174       if data.get("GENERATE_" + k, v[0]) == "YES":
175          # Grmpf ... need to use a File object here. The problem is, that
176          # Dir.scan() is implemented to just return the directory entries
177          # and does *not* invoke the source-file scanners .. ARGH !!
178          dir = env.Dir( os.path.join(str(source[0].dir), out_dir, data.get(k + "_OUTPUT", v[1])) )
179          node = env.File( os.path.join(str(dir), ".stamp" ) )
180          env.Clean(node, dir)
181          targets.append( node )
182
183    if data.has_key("GENERATE_TAGFILE"):
184       targets.append(env.File( os.path.join(str(source[0].dir), data["GENERATE_TAGFILE"]) ))
185
186    # don't clobber targets
187    for node in targets:
188       env.Precious(node)
189
190    return (targets, source)
191
192 def doxyNodeHtmlDir(node):
193    if not node.sources : return None
194    data = DoxyfileParse(node.sources[0].get_contents(), str(node.sources[0].dir))
195    if data.get("GENERATE_HTML",'YES') != 'YES' : return None
196    return os.path.normpath(os.path.join( str(node.sources[0].dir),
197                                          data.get("OUTPUT_DIRECTORY","."),
198                                          data.get("HTML_OUTPUT","html") ))
199
200 def relpath(source, target):
201    source = os.path.normpath(source)
202    target = os.path.normpath(target)
203    prefix = os.path.dirname(os.path.commonprefix((source,target)))
204    prefix_len = prefix and len(prefix.split(os.sep)) or 0
205    source_elts = source.split(os.sep)
206    target_elts = target.split(os.sep)
207    if source_elts[0] == '..' or target_elts[0] == '..':
208       raise ValueError, "invalid relapth args"
209    return os.path.join(*([".."] * (len(source_elts) - prefix_len) +
210                          target_elts[prefix_len:]))
211
212 def DoxyGenerator(source, target, env, for_signature):
213
214    data = DoxyfileParse(source[0].get_contents(), str(source[0].dir))
215
216    actions = [ env.Action("cd ${SOURCE.dir}  &&  ${DOXYGEN} ${SOURCE.file}") ]
217
218    # This will add automatic 'installdox' calls.
219    #
220    # For every referenced tagfile, the generator first checks for the
221    # existence of a construction variable '<name>_DOXY_URL' where
222    # '<name>' is the uppercased name of the tagfile sans extension
223    # (e.g. 'Utils.tag' -> 'UTILS_DOXY_URL'). If this variable exists,
224    # it must contain the url or path to the installed documentation
225    # corresponding to the tag file.
226    #
227    # Is the variable is not found and if a referenced tag file is a
228    # target within this same build, the generator will parse the
229    # 'Doxyfile' from which the tag file is built. It will
230    # automatically create the html directory from the information in
231    # that 'Doxyfile'.
232    #
233    # If for any referenced tagfile no url can be found, 'installdox'
234    # will *not* be called and a warning about the missing url is
235    # generated.
236    
237    if data.get('GENERATE_HTML','YES') == "YES":
238       output_dir = os.path.normpath(os.path.join( str(source[0].dir),
239                                                   data.get("OUTPUT_DIRECTORY","."),
240                                                   data.get("HTML_OUTPUT","html") ))
241       args = []
242       for tagfile in data.get('TAGFILES',[]):
243          url = env.get(os.path.splitext(os.path.basename(tagfile))[0].upper()+"_DOXY_URL", None)
244          if not url:
245             url = doxyNodeHtmlDir(
246                env.File(os.path.normpath(os.path.join( str(source[0].dir), tagfile ))))
247             if url : url = relpath(output_dir, url)
248          if not url:
249             print "WARNING:",str(node.sources[0]),": missing tagfile url for",tagfile
250             args = None
251          if args is not None and url:
252             args.append("-l %s@%s" % ( os.path.basename(tagfile), url ))
253       if args:
254          actions.append(env.Action('cd %s && ./installdox %s' % (output_dir, " ".join(args))))
255    
256    actions.append(env.Action([ "touch $TARGETS" ]))
257
258    return actions
259
260 def generate(env):
261    """
262    Add builders and construction variables for the
263    Doxygen tool.  This is currently for Doxygen 1.4.6.
264    """
265    doxyfile_scanner = env.Scanner(
266       DoxySourceScan,
267       "DoxySourceScan",
268       scan_check = DoxySourceScanCheck,
269    )
270
271    doxyfile_builder = env.Builder(
272       # scons 0.96.93 hang on the next line but I don't know hot to FIX the problem
273       generator = DoxyGenerator,
274       emitter = DoxyEmitter,
275       target_factory = env.fs.Entry,
276       single_source = True,
277       source_scanner =  doxyfile_scanner
278    )
279
280    env.Append(BUILDERS = {
281       'Doxygen': doxyfile_builder,
282    })
283
284    env.AppendUnique(
285       DOXYGEN = 'doxygen',
286    )
287
288 def exists(env):
289    """
290    Make sure doxygen exists.
291    """
292    return env.Detect("doxygen")