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