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