LOOS  v2.3.2
doxygen.py
1 # vim: set et sw=3 tw=0 fo=awqorc ft=python:
2 # -*- mode:python; coding:utf-8; -*-
3 #
4 # Astxx, the Asterisk C++ API and Utility Library.
5 # Copyright © 2005, 2006 Matthew A. Nicholson
6 # Copyright © 2006 Tim Blechmann
7 #
8 # Copyright © 2007 Christoph Boehme
9 #
10 # Copyright © 2012 Dirk Baechle
11 #
12 # Copyright © 2013 Russel Winder
13 #
14 # This library is free software; you can redistribute it and/or
15 # modify it under the terms of the GNU Lesser General Public
16 # License version 2.1 as published by the Free Software Foundation.
17 #
18 # This library is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # Lesser General Public License for more details.
22 #
23 # You should have received a copy of the GNU Lesser General Public
24 # License along with this library; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
26 
27 import os
28 import os.path
29 import glob
30 from fnmatch import fnmatch
31 
32 # Currently supported output formats and their default
33 # values and output locations.
34 # From left to right:
35 # 1. default setting YES|NO
36 # 2. default output folder for this format
37 # 3. name of the (main) output file
38 # 4. default extension "
39 # 5. field for overriding the output file extension
40 output_formats = {
41  "HTML": ("YES", "html", "index", ".html", "HTML_FILE_EXTENSION"),
42  "LATEX": ("YES", "latex", "refman", ".tex", ""),
43  "RTF": ("NO", "rtf", "refman", ".rtf", ""),
44  "MAN": ("NO", "man", "", ".3", "MAN_EXTENSION"),
45  "XML": ("NO", "xml", "index", ".xml", ""),
46 }
47 
48 
49 def DoxyfileParse(file_contents, conf_dir, data=None):
50  """
51  Parse a Doxygen source file and return a dictionary of all the values.
52  Values will be strings and lists of strings.
53  """
54  if data is None:
55  data = {}
56 
57  import shlex
58  lex = shlex.shlex(instream=file_contents, posix=True)
59  lex.wordchars += "*+./-:@"
60  lex.whitespace = lex.whitespace.replace("\n", "")
61  lex.escape = ""
62 
63  lineno = lex.lineno
64  token = lex.get_token()
65  key = None
66  last_token = ""
67  key_token = True # The first token should be a key.
68  next_key = False
69  new_data = True
70 
71  def append_data(data, key, new_data, token):
72  if new_data or len(data[key]) == 0:
73  data[key].append(token)
74  else:
75  data[key][-1] += token
76 
77  while token:
78  if token in ['\n']:
79  if last_token not in ['\\']:
80  key_token = True
81  elif token in ['\\']:
82  pass
83  elif key_token:
84  key = token
85  key_token = False
86  else:
87  if token == "+=":
88  if key not in data:
89  data[key] = []
90  elif token == "=":
91  if key == "TAGFILES" and key in data:
92  append_data(data, key, False, "=")
93  new_data = False
94  elif key == "@INCLUDE" and key in data:
95  # don't reset the @INCLUDE list when we see a new @INCLUDE line.
96  pass
97  else:
98  data[key] = []
99  elif key == "@INCLUDE":
100  # special case for @INCLUDE key: read the referenced
101  # file as a doxyfile too.
102  nextfile = token
103  if not os.path.isabs(nextfile):
104  nextfile = os.path.join(conf_dir, nextfile)
105  if nextfile in data[key]:
106  raise Exception("recursive @INCLUDE in Doxygen config: " + nextfile)
107  data[key].append(nextfile)
108  fh = open(nextfile, 'r')
109  DoxyfileParse(fh.read(), conf_dir, data)
110  fh.close()
111  else:
112  append_data(data, key, new_data, token)
113  new_data = True
114 
115  last_token = token
116  token = lex.get_token()
117 
118  if last_token == '\\' and token != '\n':
119  new_data = False
120  append_data(data, key, new_data, '\\')
121 
122  # compress lists of len 1 into single strings
123  for (k, v) in data.items():
124  if len(v) == 0:
125  data.pop(k)
126 
127  # items in the following list will be kept as lists and not converted to strings
128  if k in ["INPUT", "FILE_PATTERNS", "EXCLUDE_PATTERNS", "TAGFILES", "@INCLUDE"]:
129  continue
130 
131  if len(v) == 1:
132  data[k] = v[0]
133 
134  return data
135 
136 
137 def DoxySourceFiles(node, env):
138  """
139  Scan the given node's contents (a Doxygen file) and add
140  any files used to generate docs to the list of source files.
141  """
142  default_file_patterns = [
143  '*.c', '*.cc', '*.cxx', '*.cpp', '*.c++', '*.java', '*.ii', '*.ixx',
144  '*.ipp', '*.i++', '*.inl', '*.h', '*.hh ', '*.hxx', '*.hpp', '*.h++',
145  '*.idl', '*.odl', '*.cs', '*.php', '*.php3', '*.inc', '*.m', '*.mm',
146  '*.py',
147  ]
148 
149  default_exclude_patterns = [
150  '*~',
151  ]
152 
153  sources = []
154 
155  # We're running in the top-level directory, but the doxygen
156  # configuration file is in the same directory as node; this means
157  # that relative pathnames in node must be adjusted before they can
158  # go onto the sources list
159  conf_dir = os.path.dirname(str(node))
160 
161  data = DoxyfileParse(node.get_contents(), conf_dir)
162 
163  if data.get("RECURSIVE", "NO") == "YES":
164  recursive = True
165  else:
166  recursive = False
167 
168  file_patterns = data.get("FILE_PATTERNS", default_file_patterns)
169  exclude_patterns = data.get("EXCLUDE_PATTERNS", default_exclude_patterns)
170 
171  input = data.get("INPUT")
172  if input:
173  for node in data.get("INPUT", []):
174  if not os.path.isabs(node):
175  node = os.path.join(conf_dir, node)
176  if os.path.isfile(node):
177  sources.append(node)
178  elif os.path.isdir(node):
179  if recursive:
180  for root, dirs, files in os.walk(node):
181  for f in files:
182  filename = os.path.join(root, f)
183 
184  pattern_check = reduce(lambda x, y: x or bool(fnmatch(filename, y)), file_patterns, False)
185  exclude_check = reduce(lambda x, y: x and fnmatch(filename, y), exclude_patterns, True)
186 
187  if pattern_check and not exclude_check:
188  sources.append(filename)
189  else:
190  for pattern in file_patterns:
191  sources.extend(glob.glob("/".join([node, pattern])))
192  else:
193  # No INPUT specified, so apply plain patterns only
194  if recursive:
195  for root, dirs, files in os.walk('.'):
196  for f in files:
197  filename = os.path.join(root, f)
198 
199  pattern_check = reduce(lambda x, y: x or bool(fnmatch(filename, y)), file_patterns, False)
200  exclude_check = reduce(lambda x, y: x and fnmatch(filename, y), exclude_patterns, True)
201 
202  if pattern_check and not exclude_check:
203  sources.append(filename)
204  else:
205  for pattern in file_patterns:
206  sources.extend(glob.glob(pattern))
207 
208  # Add @INCLUDEd files to the list of source files:
209  for node in data.get("@INCLUDE", []):
210  sources.append(node)
211 
212  # Add tagfiles to the list of source files:
213  for node in data.get("TAGFILES", []):
214  file = node.split("=")[0]
215  if not os.path.isabs(file):
216  file = os.path.join(conf_dir, file)
217  sources.append(file)
218 
219  # Add additional files to the list of source files:
220  def append_additional_source(option, formats):
221  for f in formats:
222  if data.get('GENERATE_' + f, output_formats[f][0]) == "YES":
223  file = data.get(option, "")
224  if file != "":
225  if not os.path.isabs(file):
226  file = os.path.join(conf_dir, file)
227  if os.path.isfile(file):
228  sources.append(file)
229  break
230 
231  append_additional_source("HTML_STYLESHEET", ['HTML'])
232  append_additional_source("HTML_HEADER", ['HTML'])
233  append_additional_source("HTML_FOOTER", ['HTML'])
234 
235  return sources
236 
237 
238 def DoxySourceScan(node, env, path):
239  """
240  Doxygen Doxyfile source scanner. This should scan the Doxygen file and add
241  any files used to generate docs to the list of source files.
242  """
243  filepaths = DoxySourceFiles(node, env)
244  sources = map(lambda path: env.File(path), filepaths)
245  return sources
246 
247 
248 def DoxySourceScanCheck(node, env):
249  """Check if we should scan this file"""
250  return os.path.isfile(node.path)
251 
252 
253 def DoxyEmitter(target, source, env):
254  """Doxygen Doxyfile emitter"""
255  doxy_fpath = str(source[0])
256  conf_dir = os.path.dirname(doxy_fpath)
257  data = DoxyfileParse(source[0].get_contents(), conf_dir)
258 
259  targets = []
260  out_dir = data.get("OUTPUT_DIRECTORY", ".")
261  if not os.path.isabs(out_dir):
262  out_dir = os.path.join(conf_dir, out_dir)
263 
264  # add our output locations
265  for (k, v) in output_formats.items():
266  if data.get("GENERATE_" + k, v[0]) == "YES":
267  # Initialize output file extension for MAN pages
268  if k == 'MAN':
269  # Is the given extension valid?
270  manext = v[3]
271  if v[4] and v[4] in data:
272  manext = data.get(v[4])
273  # Try to strip off dots
274  manext = manext.replace('.', '')
275  # Can we convert it to an int?
276  try:
277  e = int(manext)
278  except:
279  # No, so set back to default
280  manext = "3"
281 
282  od = env.Dir(os.path.join(out_dir, data.get(k + "_OUTPUT", v[1]), "man" + manext))
283  else:
284  od = env.Dir(os.path.join(out_dir, data.get(k + "_OUTPUT", v[1])))
285  # don't clobber target folders
286  env.Precious(od)
287  # set up cleaning stuff
288  env.Clean(od, od)
289 
290  # Add target files
291  if k != "MAN":
292  # Is an extension override var given?
293  if v[4] and v[4] in data:
294  fname = v[2] + data.get(v[4])
295  else:
296  fname = v[2] + v[3]
297  of = env.File(os.path.join(out_dir, data.get(k + "_OUTPUT", v[1]), fname))
298  targets.append(of)
299  # don't clean single files, we remove the complete output folders (see above)
300  env.NoClean(of)
301  else:
302  # Special case: MAN pages
303  # We have to add a target file docs/man/man3/foo.h.3
304  # for each input file foo.h, so we scan the config file
305  # a second time... :(
306  filepaths = DoxySourceFiles(source[0], env)
307  for f in filepaths:
308  if os.path.isfile(f) and f != doxy_fpath:
309  of = env.File(os.path.join(out_dir,
310  data.get(k + "_OUTPUT", v[1]),
311  "man" + manext,
312  f + "." + manext))
313  targets.append(of)
314  # don't clean single files, we remove the complete output folders (see above)
315  env.NoClean(of)
316 
317  # add the tag file if neccessary:
318  tagfile = data.get("GENERATE_TAGFILE", "")
319  if tagfile != "":
320  if not os.path.isabs(tagfile):
321  tagfile = os.path.join(conf_dir, tagfile)
322  targets.append(env.File(tagfile))
323 
324  return (targets, source)
325 
326 
327 def generate(env):
328  """
329  Add builders and construction variables for the
330  Doxygen tool. This is currently for Doxygen 1.4.6.
331  """
332  doxyfile_scanner = env.Scanner(
333  DoxySourceScan,
334  "DoxySourceScan",
335  scan_check=DoxySourceScanCheck,
336  )
337 
338  import SCons.Builder
339  doxyfile_builder = SCons.Builder.Builder(
340  action="cd ${SOURCE.dir} && ${DOXYGEN} ${SOURCE.file}",
341  emitter=DoxyEmitter,
342  target_factory=env.fs.Entry,
343  single_source=True,
344  source_scanner=doxyfile_scanner,
345  )
346 
347  env.Append(BUILDERS={
348  'Doxygen': doxyfile_builder,
349  })
350 
351  env.AppendUnique(
352  DOXYGEN='doxygen',
353  )
354 
355 
356 def exists(env):
357  """
358  Make sure doxygen exists.
359  """
360  return env.Detect("doxygen")