Update stuff for old BlueGene
[alexxy/gromacs.git] / admin / includedeps.py
1 #!/usr/bin/python
2 #
3 # This file is part of the GROMACS molecular simulation package.
4 #
5 # Copyright (c) 2012,2013,2014, by the GROMACS development team, led by
6 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7 # and including many others, as listed in the AUTHORS file in the
8 # top-level source directory and at http://www.gromacs.org.
9 #
10 # GROMACS is free software; you can redistribute it and/or
11 # modify it under the terms of the GNU Lesser General Public License
12 # as published by the Free Software Foundation; either version 2.1
13 # of the License, or (at your option) any later version.
14 #
15 # GROMACS is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18 # Lesser General Public License for more details.
19 #
20 # You should have received a copy of the GNU Lesser General Public
21 # License along with GROMACS; if not, see
22 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
23 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
24 #
25 # If you want to redistribute modifications to GROMACS, please
26 # consider that scientific software is very special. Version
27 # control is crucial - bugs must be traceable. We will be happy to
28 # consider code for inclusion in the official distribution, but
29 # derived work must not be called official GROMACS. Details are found
30 # in the README & COPYING files - if they are missing, get the
31 # official version at http://www.gromacs.org.
32 #
33 # To help us fund GROMACS development, we humbly ask that you cite
34 # the research papers on the package. Check out http://www.gromacs.org.
35
36 """Check and generate include dependency graphs for Gromacs.
37
38 This script can do a few things related to include file dependencies:
39  - Check that there are no broken dependencies between installed headers.
40  - Check that documentated usage of a header matches its installation status
41    and usage from other modules.
42  - Generate two types of include dependency graphs: per-file or per-module
43    (where module is equivalent to a subdirectory).
44 It is intended to be run on a subset of files under the src/ directory.
45 Output format for the graphs is suitable for processing with 'dot'.
46
47 FILE GRAPHS
48
49 The legend for per-file graph nodes:
50     gray:          source files
51     light blue:    public headers
52     dark blue:     library headers
53     no background: other files
54
55 MODULE GRAPHS
56
57 Module graph will contain one node for each top-level subdirectory under src/,
58 except that the src/gromacs/ directory will be expanded one level further.
59 Legacy modules have gray background.
60
61 The legend for per-module graph links (a link with a certain color indicates
62 that types above it in the list are not present):
63     red:          invalid dependency
64     grey:         legacy dependency (dependency on undocumented file, or to
65                   legacy directories)
66     solid black:  public header depends on the other module
67     solid blue:   library header depends on the other module
68     dashed blue:  source file depends on a library header in the other module
69     dashed black: source file depends on a public header in the other module
70     dashed green: test file depends on the other module
71 """
72
73 import os.path
74 import re
75 import sys
76
77 from optparse import OptionParser
78
79 class ErrorReporter(object):
80     def input_warning(self, file_path, msg):
81         sys.stderr.write('warning: {0}: {1}\n'.format(file_path, msg))
82
83     def error(self, file_path, msg):
84         sys.stderr.write('error: {0}: {1}\n'.format(file_path, msg))
85
86 class Link(object):
87
88     """Link between two node objects.
89
90     Signifies an include dependency between the two nodes, and manages types
91     associated with the dependencies.
92     """
93
94     _priorities = {
95             'undocumented': 1,
96             'legacy': 2,
97             'intramodule': 3,
98             'public': 4,
99             'library': 5,
100             'libimpl': 6,
101             'pubimpl': 7,
102             'test': 8}
103
104     def __init__(self, fromnode, tonode, link_type):
105         self.fromnode = fromnode
106         self.tonode = tonode
107         self.link_type = link_type
108         if link_type not in Link._priorities:
109             raise ValueError('Unknown link type {0}'.format(link_type))
110
111     def merge_link(self, other):
112         """Merge another link into this one and choose an appropriate type.
113
114         Updates the type of this link based on the types of the merged links.
115         """
116         if Link._priorities[other.link_type] < Link._priorities[self.link_type]:
117             self.link_type = other.link_type
118
119     def format(self):
120         """Format this link for 'dot'."""
121         if self.fromnode.is_file_node() and self.tonode.is_file_node():
122             properties = ''
123         elif self.link_type == 'intramodule':
124             properties = ''
125         elif self.link_type == 'test':
126             properties = 'color=".33 .8 .8", style=dashed'
127         elif self.link_type == 'libimpl':
128             properties = 'color=".66 .8 .8", style=dashed'
129         elif self.link_type == 'pubimpl':
130             properties = 'color=black, style=dashed'
131         elif self.link_type == 'library':
132             properties = 'color=".66 .8 .8"'
133         elif self.link_type == 'public':
134             properties = 'color=black'
135         elif self.link_type == 'legacy':
136             properties = 'color=grey75'
137         else: # undocumented
138             properties = 'color=red'
139         return '{0} -> {1} [{2}]'.format(self.fromnode.nodename,
140                                          self.tonode.nodename,
141                                          properties)
142
143 class Node(object):
144     def __init__(self, nodename, label, properties, is_file):
145         self.nodename = nodename
146         self.label = label
147         self._properties = properties
148         self._is_file = is_file
149         self.children = []
150         self.root = False
151
152     def set_root(self):
153         self.root = True
154
155     def add_child(self, child):
156         self.children.append(child)
157
158     def remove_child(self, child):
159         self.children.remove(child)
160
161     def clear_children(self):
162         self.children = []
163
164     def is_file_node(self):
165         return self._is_file
166
167     def get_children(self, recursive=False):
168         if recursive:
169             result = list(self.children)
170             for child in self.children:
171                 result.extend(child.get_children(recursive=True))
172             return result
173         else:
174             return self.children
175
176     def format(self):
177         """Format this node for 'dot'."""
178         result = ''
179         if self.children:
180             if not self.root:
181                 result += '    subgraph cluster_{0} {{\n' \
182                               .format(self.nodename)
183                 result += '        label = "{0}"\n'.format(self.label)
184             for child in self.children:
185                 result += child.format()
186             if not self.root:
187                 result += '    }\n'
188         else:
189             properties = 'label="{0}"'.format(self.label)
190             if self._properties:
191                 properties += ', ' + self._properties
192             result += '    {0} [{1}]\n'.format(self.nodename, properties)
193         return result
194
195
196 class Graph(object):
197     def __init__(self, nodes, links):
198         self.nodes = set(nodes)
199         self.links = links
200         self.left_to_right = False
201         self.concentrate = True
202
203     def set_options(self, left_to_right=None, concentrate=None):
204         if left_to_right != None:
205             self.left_to_right = left_to_right
206         if concentrate != None:
207             self.concentrate = concentrate
208
209     def prune_links(self):
210         nodes = set()
211         for node in self.nodes:
212             nodes.update(node.get_children(recursive=True))
213         newlinks = []
214         for link in self.links:
215             if link.fromnode in nodes and link.tonode in nodes:
216                 newlinks.append(link)
217         self.links = newlinks
218
219     def merge_nodes(self, nodes, target):
220         nodes = set(nodes)
221         nodes.add(target)
222         newlinks = []
223         linksto = dict()
224         linksfrom = dict()
225         for link in self.links:
226             isfrom = (link.fromnode in nodes)
227             isto = (link.tonode in nodes)
228             if isfrom and isto:
229                 pass
230             elif isfrom:
231                 if not link.tonode in linksfrom:
232                     linksfrom[link.tonode] = \
233                             Link(target, link.tonode, link.link_type)
234                 else:
235                     linksfrom[link.tonode].merge_link(link)
236             elif isto:
237                 if not link.fromnode in linksto:
238                     linksto[link.fromnode] = \
239                             Link(link.fromnode, target, link.link_type)
240                 else:
241                     linksto[link.fromnode].merge_link(link)
242             else:
243                 newlinks.append(link)
244         newlinks.extend(linksfrom.values())
245         newlinks.extend(linksto.values())
246         self.links = newlinks
247
248     def collapse_node(self, node):
249         nodes = node.get_children(recursive=True)
250         self.merge_nodes(nodes, node)
251         node.clear_children()
252
253     def write(self, outfile):
254         outfile.write('digraph includedeps {\n')
255         if self.left_to_right:
256             outfile.write('    rankdir = LR\n')
257         if self.concentrate:
258             outfile.write('    concentrate = true\n')
259         outfile.write('    node [fontname="FreeSans",fontsize=10,height=.2,'
260                                  'shape=box]\n')
261         for link in self.links:
262             outfile.write('    ' + link.format() + '\n')
263         for node in self.nodes:
264             outfile.write(node.format())
265         outfile.write('}\n')
266
267
268 def find_include_file(filename, includedirs):
269     """Find full path to filename, looking in a set of directories."""
270     for includedir in includedirs:
271         fullpath = os.path.abspath(os.path.join(includedir, filename))
272         if os.path.exists(fullpath):
273             return fullpath
274     return None
275
276
277 class IncludedFile(object):
278     def __init__(self, included_file, included_path, is_relative, is_system):
279         self._included_file = included_file
280         self._included_path = included_path
281         #self._used_include_path = used_include_path
282         self._is_relative = is_relative
283         self._is_system = is_system
284
285
286 class File(object):
287     def __init__(self, path, module):
288         self.path = path
289         self.name = os.path.basename(path)
290         self.module = module
291         if module.name == 'tests' or module.name == 'legacytests':
292             self.type = 'test'
293         elif re.search(r'\.c(pp|u)?$', self.name) != None:
294             self.type = 'source'
295         else:
296             self.type = 'header'
297         self.doctype = 'none'
298         #headername = re.sub(r'\.cpp$', '.h', self.name)
299         #implheadername = re.sub(r'\.cpp$', '-impl.h', self.name)
300         self._included = []
301         self.installed = False
302
303     def is_documented(self):
304         return self.doctype != 'none'
305
306     def is_installed(self):
307         return self.installed
308
309     def set_installed(self, reporter):
310         if self.type != 'header':
311             reporter.input_warning(self.path,
312                     'installing {0} file'.format(self.type))
313             return
314         self.installed = True
315
316     def get_included_files(self):
317         return self._included
318
319     def scan_include_file(self, line, allfiles, selfdir, includedirs,
320             ignorelist, reporter):
321         """Process #include directive during scan().
322
323         Searches for the included file in given directories, does some checks,
324         and adds the dependency link to the other file if applicable.
325         """
326         fullpath = None
327         includedpath = None
328         includedfile = None
329         is_system = False
330         is_relative = False
331         match = re.match(r'#include *<([^>]*)>', line)
332         if match:
333             includedpath = match.group(1)
334             is_system = True
335             fullpath = find_include_file(includedpath, includedirs)
336         else:
337             match = re.match(r'#include *"([^"]*)"', line)
338             if match:
339                 includedpath = match.group(1)
340                 fullpath = os.path.abspath(os.path.join(selfdir, includedpath))
341                 #if os.path.abspath(fullpath) in ignorelist:
342                 #    return
343                 if os.path.exists(fullpath):
344                     is_relative = True
345                 else:
346                     fullpath = find_include_file(includedpath, includedirs)
347                     if not fullpath:
348                         if not includedpath in ('corewrap.h', 'tmpi_config.h'):
349                             reporter.input_warning(self.path,
350                                     'included file "{0}" not found'
351                                         .format(includedpath))
352         if not includedpath:
353             reporter.input_warning(self.path, 'line "{0}" could not be parsed'
354                     .format(line))
355         else:
356             if fullpath and fullpath in allfiles:
357                 includedfile = allfiles[fullpath]
358             #elif not dep in ignorelist:
359             #    depfile = File(dep, None)
360             #    files[dep] = depfile
361             #    file.add_dependency(depfile)
362             #    extrafiles.append(dep)
363             self._included.append(IncludedFile(includedfile, includedpath,
364                     is_relative, is_system))
365
366     def scan(self, filename, allfiles, includedirs, ignorelist, reporter):
367         selfdir = os.path.dirname(filename)
368         infileblock = False
369         foundfileblock = False
370         self.docmodule = None
371         with open(filename, 'r') as scanfile:
372             for line in scanfile:
373                 if line.startswith('#include'):
374                     self.scan_include_file(line, allfiles, selfdir,
375                             includedirs, ignorelist, reporter)
376                     continue
377                 if not foundfileblock:
378                     if infileblock:
379                         if r'*/' in line:
380                             infileblock = False
381                             foundfileblock = True
382                             continue
383                         if self.type == 'implheader':
384                             if line.startswith(r' * \inpublicapi'):
385                                 self.type = 'publicheader'
386                             elif line.startswith(r' * \inlibraryapi'):
387                                 self.type = 'libheader'
388                         match = re.match(r' \* \\ingroup module_([a-z_]*)', line)
389                         if match:
390                             if self.docmodule:
391                                 reporter.error(self.path,
392                                         'file documented in multiple modules')
393                             self.docmodule = match.group(1)
394                     else:
395                         match = re.match(r'/\*! *(\\[a-z]*internal)? *\\file', line)
396                         if match:
397                             docspec = match.group(1)
398                             if not docspec:
399                                 self.doctype = 'public'
400                             elif docspec == r'\libinternal':
401                                 self.doctype = 'library'
402                             elif docspec == r'\internal':
403                                 self.doctype = 'implementation'
404                             else:
405                                 reporter.input_warning(self.path,
406                                         'unknown specifier "{0}"'.format(docspec))
407                                 self.doctype = 'unknown'
408                             infileblock = True
409                             if self.type == 'header':
410                                 # Default type if no other found
411                                 self.type = 'implheader'
412
413
414 class Module(object):
415     def __init__(self, name, parent = None):
416         self.parent = parent
417         self.name = name
418         if parent:
419             self.fullname = parent.fullname + '_' + name
420         else:
421             self.fullname = 'module'
422         self.files = []
423         self.children = dict()
424         self.is_top_level = (not parent or parent.name in ('', 'gromacs'))
425
426     def get_parent(self):
427         return self.parent
428
429     def is_child(self, module):
430         parent = module.parent
431         while parent:
432             if parent == self:
433                 return True
434             parent = parent.parent
435         return False
436
437     def get_top_level_module(self):
438         if self.is_top_level or not self.parent:
439             return self
440         return self.parent.get_top_level_module()
441
442     def add_nested_file(self, modules, path):
443         if len(modules) == 1:
444             newfile = File(path, self)
445             self.files.append(newfile)
446         else:
447             if not modules[0] in self.children:
448                 module = Module(modules[0], self)
449                 self.children[modules[0]] = module
450             else:
451                 module = self.children[modules[0]]
452             newfile = module.add_nested_file(modules[1:], path)
453         return newfile
454
455
456 class Dependencies(object):
457     def __init__(self, rootdir, includedirs, installedfiles):
458         self.files = dict()
459         self.root = Module("")
460         self.rootpath = []
461         for root in rootdir:
462             self.rootpath.append(os.path.abspath(root))
463         if includedirs:
464             self.includedirs = self.rootpath + includedirs
465         else:
466             self.includedirs = self.rootpath
467         self.installedfiles = installedfiles
468
469     def add_file(self, filename, reporter):
470         fullpath = os.path.abspath(filename)
471         for root in self.rootpath:
472             if fullpath.startswith(root):
473                 relpath = fullpath[len(root)+1:]
474                 break
475         else:
476             reporter.input_warning(filename,
477                     'input file not under root path, skipped')
478             return
479         modules = relpath.split(os.sep)
480         newfile = self.root.add_nested_file(modules, relpath)
481         if fullpath in self.installedfiles:
482             newfile.set_installed(reporter)
483         self.files[os.path.abspath(filename)] = newfile
484
485     def scan_files(self, ignorelist, reporter):
486         for (filename, scanfile) in self.files.iteritems():
487             scanfile.scan(filename, self.files, self.includedirs, ignorelist,
488                     reporter)
489
490     def get_toplevel_modules(self):
491         result = []
492         for module in self.root.children.itervalues():
493             if module.name == 'gromacs':
494                 result.extend(module.children.itervalues())
495             else:
496                 result.append(module)
497         return result
498
499
500 def _is_legacy_module(module):
501     if module.name in ('legacyheaders', 'gmxlib', 'mdlib', 'gmxana', 'gmxpreprocess'):
502         return True
503     if module.get_parent():
504         return _is_legacy_module(module.get_parent())
505     return False
506
507
508 class IncludeFileChecker(object):
509     def __init__(self, deps, options):
510         self._deps = deps
511         self._options = options
512
513     def _check_file(self, checkfile, reporter):
514         if not self._options.check_doc:
515             return
516         if not checkfile.is_documented():
517             if self._options.warn_undoc:
518                 is_legacy = _is_legacy_module(checkfile.module)
519                 is_external = checkfile.module.name in ('gmx_lapack', 'gmx_blas', 'thread_mpi')
520                 if not is_legacy and not is_external:
521                     reporter.error(checkfile.path, 'file not documented')
522         elif checkfile.doctype == 'implementation' and \
523                 checkfile.type in ('publicheader', 'libheader'):
524             reporter.error(checkfile.path,
525                     'file documentation visibility incorrect')
526         elif checkfile.doctype == 'library' and checkfile.type == 'publicheader':
527             reporter.error(checkfile.path,
528                     'file documentation visibility incorrect')
529         elif checkfile.installed and checkfile.doctype not in ('public', 'unknown'):
530             reporter.error(checkfile.path,
531                     'installed header has no public documentation')
532         elif not checkfile.installed and checkfile.doctype == 'public':
533             reporter.error(checkfile.path,
534                     'non-installed file has public documentation')
535         selfmodfullname = checkfile.module.fullname
536         docmodule = checkfile.docmodule
537         if docmodule and \
538                 not selfmodfullname.startswith('module_' + docmodule) and \
539                 not selfmodfullname.startswith('module_gromacs_' + docmodule) and \
540                 not checkfile.name == docmodule + '.h':
541             reporter.error(checkfile.path,
542                     'file documented in incorrect module "{0}"'
543                         .format(docmodule))
544
545     def _check_included_file(self, checkfile, includedfile, reporter):
546         otherfile = includedfile._included_file
547         if includedfile._is_system:
548             # TODO: This doesn't report errors with files not listed in
549             # the input files, although those could be included.
550             # That would produce a massive amount of errors for <config.h>.
551             if otherfile:
552                 reporter.error(checkfile.path,
553                         'local file included as <{0}>'
554                             .format(includedfile._included_path))
555         elif not includedfile._is_relative and checkfile.installed:
556             if not includedfile._included_path == 'gmx_header_config_gen.h':
557                 reporter.error(checkfile.path,
558                         'installed header includes "{0}", '
559                         'which is not found using relative path'
560                             .format(includedfile._included_path))
561         if not otherfile:
562             return
563         if checkfile.installed and not otherfile.installed:
564             reporter.error(checkfile.path,
565                     'installed header includes '
566                     'non-installed header "{0}"'
567                         .format(includedfile._included_path))
568         if not otherfile.is_documented():
569             return
570         if not self._options.check_doc:
571             return
572         intramodule = \
573                 (checkfile.module.get_top_level_module() == \
574                  otherfile.module.get_top_level_module())
575         if otherfile.type not in ('publicheader', 'libheader', 'test'):
576             if not intramodule and not _is_legacy_module(otherfile.module):
577                 reporter.error(checkfile.path,
578                         'included file "{0}" is missing API definition'
579                             .format(otherfile.path))
580         elif checkfile.type == 'publicheader':
581             if not otherfile.type == 'publicheader' and not otherfile.doctype == 'public':
582                 reporter.error(checkfile.path,
583                         'public API file includes non-public header "{0}"'
584                             .format(otherfile.path))
585
586     def check_all(self, reporter):
587         for checkfile in sorted(self._deps.files.values()):
588             self._check_file(checkfile, reporter)
589             for includedfile in checkfile.get_included_files():
590                 self._check_included_file(checkfile, includedfile, reporter)
591
592
593 class GraphBuilder(object):
594     def __init__(self, deps):
595         self._deps = deps
596
597     def _create_file_node(self, fileobj, filenodes):
598         nodename = re.subn(r'[-./]', '_', fileobj.path)[0]
599         properties = []
600         style = []
601         properties.append('URL="\\ref {0}"'.format(fileobj.name))
602         if not fileobj.module:
603             style.append('bold')
604             properties.append('color=red')
605         if fileobj.type == 'source':
606             style.append('filled')
607             properties.append('fillcolor=grey75')
608         elif fileobj.type == 'publicheader':
609             style.append('filled')
610             properties.append('fillcolor=".66 .2 1"')
611         elif fileobj.type == 'libheader':
612             style.append('filled')
613             properties.append('fillcolor=".66 .5 1"')
614         if style:
615             properties.append('style="{0}"'.format(','.join(style)))
616         node = Node(nodename, fileobj.name, ', '.join(properties), is_file=True)
617         filenodes[fileobj] = node
618         return node
619
620     def _create_file_edge(self, fromfile, tofile, filenodes):
621         intramodule = \
622                 (fromfile.module.get_top_level_module() == \
623                  tofile.module.get_top_level_module())
624         is_legacy = _is_legacy_module(tofile.module)
625         if tofile.type not in ('publicheader', 'libheader', 'header', 'test'):
626             if intramodule:
627                 link_type = 'intramodule'
628             elif is_legacy:
629                 link_type = 'legacy'
630             else:
631                 link_type = 'undocumented'
632         elif fromfile.type == 'test':
633             link_type = 'test'
634         elif tofile.type == 'test':
635             link_type = 'undocumented'
636         elif fromfile.type in ('source', 'header', 'implheader') and \
637                 not fromfile.is_installed():
638             if intramodule:
639                 link_type = 'intramodule'
640             elif tofile.type == 'publicheader':
641                 link_type = 'pubimpl'
642             elif tofile.type == 'libheader':
643                 link_type = 'libimpl'
644             elif is_legacy:
645                 link_type = 'legacy'
646             elif not tofile.is_documented():
647                 link_type = 'legacy'
648             else:
649                 raise ValueError('Unknown link type between {0} and {1}'
650                         .format(fromfile.path, tofile.path))
651         elif fromfile.type == 'libheader':
652             link_type = 'library'
653         elif fromfile.type == 'publicheader' or fromfile.is_installed():
654             if tofile.type == 'publicheader' or tofile.doctype == 'public' or \
655                     (tofile.is_installed() and not tofile.is_documented()):
656                 link_type = 'public'
657             else:
658                 link_type = 'undocumented'
659         else:
660             raise ValueError('Unknown link type between {0} and {1}'
661                     .format(fromfile.path, tofile.path))
662         return Link(filenodes[fromfile], filenodes[tofile], link_type)
663
664     def _create_file_edges(self, fileobj, filenodes):
665         links = []
666         if fileobj in filenodes:
667             for includedfile in fileobj.get_included_files():
668                 otherfile = includedfile._included_file
669                 if otherfile and otherfile in filenodes:
670                     link = self._create_file_edge(fileobj, otherfile, filenodes)
671                     links.append(link)
672         return links
673
674     def create_module_node(self, module, filenodes):
675         properties = 'shape=ellipse, URL="\\ref module_{0}"'.format(module.name)
676         if _is_legacy_module(module):
677             properties += 'style=filled, fillcolor=grey75'
678         node = Node(module.fullname, module.name, properties, is_file=False)
679         for childfile in module.files:
680             node.add_child(self._create_file_node(childfile, filenodes))
681         for childmodule in module.children.itervalues():
682             node.add_child(self.create_module_node(childmodule, filenodes))
683         return node
684
685     def create_file_graph(self):
686         filenodes = dict()
687         rootnode = self.create_module_node(self._deps.root, filenodes)
688         rootnode.set_root()
689         links = []
690         for scanfile in self._deps.files.itervalues():
691             links.extend(self._create_file_edges(scanfile, filenodes))
692         graph = Graph([rootnode], links)
693         return graph
694
695     def create_modules_graph(self):
696         filenodes = dict()
697         rootnode = self.create_module_node(self._deps.root, filenodes)
698         rootnode.set_root()
699         links = []
700         for scanfile in self._deps.files.itervalues():
701             links.extend(self._create_file_edges(scanfile, filenodes))
702         graph = Graph([rootnode], links)
703         for node in rootnode.get_children():
704             if node.label == 'gromacs':
705                 module_nodes = []
706                 header_nodes = []
707                 for child in node.get_children():
708                     if child.is_file_node():
709                         header_nodes.append(child)
710                     else:
711                         graph.collapse_node(child)
712                         module_nodes.append(child)
713                 for header in header_nodes:
714                     for module in module_nodes:
715                         if header.nodename.startswith(module.nodename[7:]):
716                             # graph.merge_nodes([header], module)
717                             node.remove_child(header)
718                             break
719             else:
720                 graph.collapse_node(node)
721         graph.set_options(concentrate=False)
722         graph.prune_links()
723         return graph
724
725     def create_module_file_graph(self, module):
726         filenodes = dict()
727         rootnode = self.create_module_node(module, filenodes)
728         rootnode.set_root()
729         links = []
730         for scanfile in self._deps.files.itervalues():
731             links.extend(self._create_file_edges(scanfile, filenodes))
732         graph = Graph([rootnode], links)
733         graph.prune_links()
734         return graph
735
736
737 def print_module_graph(outfile, graphbuilder, options):
738     graph = graphbuilder.create_modules_graph()
739     graph.write(outfile)
740
741 def print_file_graph(outfile, graphbuilder, options):
742     graph = graphbuilder.create_file_graph()
743     graph.set_options(left_to_right=options.left_to_right)
744     graph.write(outfile)
745     #if options.source_at_top:
746     #    sourcenodes = []
747     #    for file in deps.files.itervalues():
748     #        if file.sourcefile:
749     #            sourcenodes.append(file.nodename)
750     #    if sourcenodes:
751     #        outfile.write('    { rank = min; ' + '; '.join(sourcenodes) + '}\n')
752     #if options.with_external and options.external_at_bottom:
753     #    extnodes = []
754     #    for file in deps.files.itervalues():
755     #        if not file.module:
756     #            extnodes.append(file.nodename)
757     #    if extnodes:
758     #        outfile.write('    { rank = max; ' + '; '.join(extnodes) + '}\n')
759
760 def print_module_file_graph(outfile, graphbuilder, module, options):
761     graph = graphbuilder.create_module_file_graph(module)
762     graph.set_options(left_to_right=options.left_to_right)
763     graph.write(outfile)
764
765 def main():
766     parser = OptionParser()
767     parser.add_option('-f', '--files',
768                       help='Read list of input files from given file')
769     parser.add_option('--installed',
770                       help='Read list of installed files from given file')
771     parser.add_option('-R', '--rootdir', action='append',
772                       help='Remove this prefix from all files')
773     parser.add_option('-I', '--includedir', action='append',
774                       help='Specify additional directories to search for '
775                            'include files')
776     parser.add_option('-o', '--outdir', default='.',
777                       help='Specify output directory for graphs')
778     #parser.add_option('--source-at-top', action='store_true',
779     #                  help='Force source files at the top of the graph')
780     #parser.add_option('--with-external', action='store_true',
781     #                  help='Include external dependencies in the graph')
782     #parser.add_option('--external-at-bottom', action='store_true',
783     #                  help='Force external dependencies files at the bottom '
784     #                       'of the graph')
785     parser.add_option('--check', action='store_true',
786                       help='Check for problems in include file dependencies')
787     parser.add_option('--check-doc', action='store_true',
788                       help='Check for problems in Doxygen documentation')
789     parser.add_option('--warn-undoc', action='store_true',
790                       help='Warn for files that do not have Doxygen documentation')
791     parser.add_option('--left-to-right', action='store_true',
792                       help='Lay out from left to right')
793     parser.add_option('--file-graph',
794                       help='Write graph for individual files')
795     parser.add_option('--module-graph',
796                       help='Write graph for modules')
797     parser.add_option('--module-file-graphs', action='store_true',
798                       help='Write file graphs for each module')
799     options, args = parser.parse_args()
800
801     if not options.file_graph and not options.module_graph and \
802             not options.module_file_graphs:
803         options.check = True
804
805     # Constructs lists of files
806     filelist = []
807     ignorelist = []
808     installedlist = []
809     if options.files:
810         with open(options.files, 'r') as outfile:
811             for line in outfile:
812                 if line.startswith('!'):
813                     ignorelist.append(os.path.abspath(line[1:].strip()))
814                 else:
815                     filelist.append(line.strip())
816     filelist.extend(args)
817     if options.installed:
818         with open(options.installed, 'r') as outfile:
819             for line in outfile:
820                 installedlist.append(line.strip())
821
822     # Creates objects for all files and modules
823     reporter = ErrorReporter()
824     deps = Dependencies(options.rootdir, options.includedir, installedlist)
825     for filename in filelist:
826         deps.add_file(filename, reporter)
827
828     deps.scan_files(ignorelist, reporter)
829
830     if options.check or options.check_doc:
831         checker = IncludeFileChecker(deps, options)
832         checker.check_all(reporter)
833
834     #if options.with_external:
835     #    for filename in extrafiles:
836     #        file = files[filename]
837     #        if os.path.exists(filename):
838     #            with open(filename, 'r') as outfile:
839     #                for line in outfile:
840     #                    if not file.api:
841     #                        if line.startswith(' * \inpublicapi'):
842     #                            file.api = "public"
843     #                        elif line.startswith(' * \inlibraryapi'):
844     #                            file.api = "library"
845
846     # Prints out the graph
847     graphbuilder = GraphBuilder(deps)
848     if options.module_graph:
849         graphpath = os.path.join(options.outdir, options.module_graph)
850         with open(graphpath, 'w') as outfile:
851             print_module_graph(outfile, graphbuilder, options)
852     if options.file_graph:
853         graphpath = os.path.join(options.outdir, options.file_graph)
854         with open(graphpath, 'w') as outfile:
855             print_file_graph(outfile, graphbuilder, options)
856     if options.module_file_graphs:
857         options.left_to_right = True
858         for module in deps.get_toplevel_modules():
859             if not _is_legacy_module(module):
860                 filename = 'module_{0}-deps.dot'.format(module.name)
861                 filename = os.path.join(options.outdir, filename)
862                 with open(filename, 'w') as outfile:
863                     print_module_file_graph(outfile, graphbuilder, module, options)
864
865 main()