3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2012, by the GROMACS development team, led by
6 # David van der Spoel, Berk Hess, Erik Lindahl, and including many
7 # others, as listed in the AUTHORS file in the top-level source
8 # directory and at http://www.gromacs.org.
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.
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.
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.
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.
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.
36 """Generate include dependency graphs for Gromacs.
38 This script can generate two types of include dependency graphs: per-file or
39 per-module (where module is equivalent to a subdirectory).
40 It is intended to be run on a subset of files under the src/ directory.
41 Output format is suitable for processing with 'dot'.
45 The legend for per-file graph nodex:
47 light blue: public headers
48 dark blue: library headers
49 no background: other files
53 Module graph will contain one node for each top-level subdirectory under src/,
54 except that the src/gromacs/ directory will be expanded one level further.
56 The legend for per-module graph links (a link with a certain color indicates
57 that types above it in the list are not present):
58 red: invalid dependency (e.g., undocumented file)
59 dark blue: library header depends on the other module
60 light blue: public header depends on the other module
61 dashed black: source file depends on a library header in the other module
62 solid black: source file depends on a public header in the other module
63 dotted grey: test files depend on the other module
70 from optparse import OptionParser
72 class ErrorReporter(object):
73 def input_warning(self, file_path, msg):
74 sys.stderr.write('warning: {0}: {1}\n'.format(file_path, msg))
76 def error(self, file_path, msg):
77 sys.stderr.write('error: {0}: {1}\n'.format(file_path, msg))
81 """Link between two node objects.
83 Signifies an include dependency between the two nodes, and manages types
84 associated with the dependencies.
97 def __init__(self, fromnode, tonode, link_type=None):
98 self.fromnode = fromnode
100 self.link_type = link_type
102 def refresh_type(self, reporter):
103 """Initialize type of a link between two file nodes.
105 Both endpoints of the link must be file objects when this method is
108 fromfile = self.fromnode.obj
109 tofile = self.tonode.obj
111 (fromfile.module.get_top_level_module() == \
112 tofile.module.get_top_level_module())
113 if tofile.type != 'publicheader' and tofile.type != 'libheader':
115 link_type = 'intramodule'
117 reporter.error(fromfile.path,
118 'included file "{0}" is missing API definition'
119 .format(tofile.path))
120 link_type = 'undocumented'
121 elif fromfile.type == 'test':
123 elif fromfile.type in ('source', 'header', 'implheader'):
124 if tofile.type == 'publicheader':
125 link_type = 'pubimpl'
126 elif tofile.type == 'libheader':
127 link_type = 'libimpl'
129 reporter.error(fromfile.path,
130 'unknown link type to "{0}"'.format(tofile.path))
131 link_type = 'unknown'
132 elif fromfile.type == 'libheader':
133 link_type = 'library'
134 elif fromfile.type == 'publicheader':
135 if tofile.type == 'publicheader' or tofile.doctype == 'public':
138 reporter.error(fromfile.path,
139 'public API file includes non-public header "{0}"'
140 .format(tofile.path))
141 link_type = 'undocumented'
143 reporter.error(fromfile.path,
144 'unknown link type to "{0}"'.format(tofile.path))
145 link_type = 'unknown'
146 self.link_type = link_type
148 def merge_link(self, other):
149 """Merge another link into this one and choose an appropriate type.
151 Updates the type of this link based on the types of the merged links.
153 if Link.priorities[other.link_type] < Link.priorities[self.link_type]:
154 self.link_type = other.link_type
157 """Format this link for 'dot'."""
158 if isinstance(self.fromnode.obj, File) and \
159 isinstance(self.tonode.obj, File):
161 elif self.link_type == 'intramodule':
163 elif self.link_type == 'test':
164 properties = 'color=grey75, style=dotted'
165 elif self.link_type == 'libimpl':
166 properties = 'color=".66 .5 1"'
167 elif self.link_type == 'pubimpl':
168 properties = 'color=".66 .2 1"'
169 elif self.link_type == 'library':
170 properties = 'color=black, style=dashed'
171 elif self.link_type == 'public':
172 properties = 'color=black'
173 else: #unknown or undocumented
174 properties = 'color=red'
175 return '{0} -> {1} [{2}]'.format(self.fromnode.obj.nodename,
176 self.tonode.obj.nodename,
180 def __init__(self, obj):
188 def add_child(self, child):
189 self.children.append(child)
191 def clear_children(self):
194 def get_children(self, recursive=False):
196 result = list(self.children)
197 for child in self.children:
198 result.extend(child.get_children(recursive=True))
204 """Format this node for 'dot'."""
208 result += ' subgraph cluster_{0} {{\n' \
209 .format(self.obj.nodename)
210 result += ' label = "{0}"\n'.format(self.obj.name)
211 for child in self.children:
212 result += child.format()
216 result += ' {0} [{1}]\n'.format(
217 self.obj.nodename, self.obj.node_properties())
222 def __init__(self, nodes, links):
223 self.nodes = set(nodes)
225 self.left_to_right = False
226 self.concentrate = True
228 def set_options(self, left_to_right=None, concentrate=None):
229 if left_to_right != None:
230 self.left_to_right = left_to_right
231 if concentrate != None:
232 self.concentrate = concentrate
234 def prune_links(self):
236 for node in self.nodes:
237 nodes.update(node.get_children(recursive=True))
239 for link in self.links:
240 if link.fromnode in nodes and link.tonode in nodes:
241 newlinks.append(link)
242 self.links = newlinks
244 def merge_nodes(self, nodes, target):
250 for link in self.links:
251 isfrom = (link.fromnode in nodes)
252 isto = (link.tonode in nodes)
256 if not link.tonode in linksfrom:
257 linksfrom[link.tonode] = \
258 Link(target, link.tonode, link.link_type)
260 linksfrom[link.tonode].merge_link(link)
262 if not link.fromnode in linksto:
263 linksto[link.fromnode] = \
264 Link(link.fromnode, target, link.link_type)
266 linksto[link.fromnode].merge_link(link)
268 newlinks.append(link)
269 newlinks.extend(linksfrom.values())
270 newlinks.extend(linksto.values())
271 self.links = newlinks
273 def collapse_node(self, node):
274 nodes = node.get_children(recursive=True)
275 self.merge_nodes(nodes, node)
276 node.clear_children()
278 def write(self, outfile):
279 outfile.write('digraph includedeps {\n')
280 if self.left_to_right:
281 outfile.write(' rankdir = LR\n')
283 outfile.write(' concentrate = true\n')
284 outfile.write(' node [fontname="FreeSans",fontsize=10,height=.2,'
286 for link in self.links:
287 outfile.write(' ' + link.format() + '\n')
288 for node in self.nodes:
289 outfile.write(node.format())
293 def find_include_file(filename, includedirs):
294 """Find full path to filename, looking in a set of directories."""
295 for includedir in includedirs:
296 fullpath = os.path.abspath(os.path.join(includedir, filename))
297 if os.path.exists(fullpath):
303 def __init__(self, path, module):
305 self.name = os.path.basename(path)
306 self.nodename = re.subn(r'[-./]', '_', path)[0]
308 if module.name == 'tests':
310 elif re.search(r'\.c(pp)?$', self.name) != None:
314 self.doctype = 'none'
315 #headername = re.sub(r'\.cpp$', '.h', self.name)
316 #implheadername = re.sub(r'\.cpp$', '-impl.h', self.name)
318 self.node = Node(self)
319 self.installed = False
321 def set_installed(self, reporter):
322 if self.type != 'header':
323 reporter.input_warning(self.path,
324 'installing {0} file'.format(self.type))
326 self.installed = True
328 def add_dependency(self, dep):
329 self.links.append(Link(self.node, dep.node))
337 def node_properties(self):
340 properties.append('label="{0}"'.format(self.name))
341 properties.append('URL="\\ref {0}"'.format(self.name))
344 properties.append('color=red')
345 if self.type == 'source':
346 style.append('filled')
347 properties.append('fillcolor=grey75')
348 elif self.type == 'publicheader':
349 style.append('filled')
350 properties.append('fillcolor=".66 .2 1"')
351 elif self.type == 'libheader':
352 style.append('filled')
353 properties.append('fillcolor=".66 .5 1"')
355 properties.append('style="{0}"'.format(','.join(style)))
356 return ', '.join(properties)
358 def scan_include_file(self, line, allfiles, selfdir, includedirs,
359 ignorelist, reporter):
360 """Process #include directive during scan().
362 Searches for the included file in given directories, does some checks,
363 and adds the dependency link to the other file if applicable.
366 match = re.match(r'#include <([^>]*)>', line)
368 includedfile = match.group(1)
369 fullpath = find_include_file(includedfile, includedirs)
371 reporter.error(self.path,
372 'local file included as <{0}>'
373 .format(includedfile))
375 match = re.match(r'#include "([^"]*)"', line)
377 includedfile = match.group(1)
378 fullpath = os.path.join(selfdir, includedfile)
379 #if os.path.abspath(fullpath) in ignorelist:
381 if not os.path.exists(fullpath):
382 fullpath = find_include_file(includedfile, includedirs)
385 reporter.error(self.path,
386 'installed header includes "{0}", '
387 'which is not found using relative path'
388 .format(includedfile))
390 reporter.input_warning(self.path,
391 'included file "{0}" not found'
392 .format(includedfile))
394 if fullpath in allfiles:
395 other = allfiles[fullpath]
396 if self.installed and not other.installed:
397 reporter.error(self.path,
398 'installed header includes '
399 'non-installed header "{0}"'
401 self.add_dependency(other)
402 #elif not dep in ignorelist:
403 # depfile = File(dep, None)
404 # files[dep] = depfile
405 # file.add_dependency(depfile)
406 # extrafiles.append(dep)
408 def scan(self, filename, allfiles, includedirs, ignorelist, reporter):
409 selfdir = os.path.dirname(filename)
411 foundfileblock = False
413 with open(filename, 'r') as scanfile:
414 for line in scanfile:
415 if line.startswith('#include'):
416 self.scan_include_file(line, allfiles, selfdir,
417 includedirs, ignorelist, reporter)
419 if not foundfileblock:
421 if line.startswith(r' */'):
423 foundfileblock = True
425 if self.type == 'implheader':
426 if line.startswith(r' * \inpublicapi'):
427 self.type = 'publicheader'
428 elif line.startswith(r' * \inlibraryapi'):
429 self.type = 'libheader'
430 match = re.match(r' \* \\ingroup module_([a-z_]*)', line)
433 reporter.error(self.path,
434 'file documented in multiple modules')
435 docmodule = match.group(1)
437 match = re.match(r'/\*! *(\\[a-z]*internal)? *\\file', line)
439 docspec = match.group(1)
441 self.doctype = 'public'
442 elif docspec == r'\libinternal':
443 self.doctype = 'library'
444 elif docspec == r'\internal':
445 self.doctype = 'implementation'
447 reporter.input_warning(self.path,
448 'unknown specifier "{0}"'.format(docspec))
449 self.doctype = 'unknown'
451 if self.type == 'header':
452 # Default type if no other found
453 self.type = 'implheader'
454 if self.doctype == 'none':
455 reporter.error(self.path, 'file not documented')
456 elif self.doctype == 'implementation' and \
457 self.type in ('publicheader', 'libheader'):
458 reporter.error(self.path,
459 'file documentation visibility incorrect')
460 elif self.doctype == 'library' and self.type == 'publicheader':
461 reporter.error(self.path,
462 'file documentation visibility incorrect')
463 if self.installed and self.doctype not in ('public', 'unknown'):
464 reporter.error(self.path,
465 'installed header has no public documentation')
466 elif not self.installed and self.doctype == 'public':
467 reporter.error(self.path,
468 'non-installed file has public documentation')
469 selfmodnodename = self.module.nodename
471 not selfmodnodename.startswith('module_' + docmodule) and \
472 not selfmodnodename.startswith('module_gromacs_' + docmodule):
473 reporter.error(self.path,
474 'file documented in incorrect module "{0}"'
478 class Module(object):
479 def __init__(self, name, parent = None):
483 self.nodename = parent.nodename + '_' + name
485 self.nodename = 'module'
487 self.children = dict()
488 self.is_top_level = (not parent or parent.name in ('', 'gromacs'))
490 def is_child(self, module):
491 parent = module.parent
495 parent = parent.parent
498 def get_top_level_module(self):
499 if self.is_top_level or not self.parent:
501 return self.parent.get_top_level_module()
503 def add_nested_file(self, modules, path):
504 if len(modules) == 1:
505 newfile = File(path, self)
506 self.files.append(newfile)
508 if not modules[0] in self.children:
509 module = Module(modules[0], self)
510 self.children[modules[0]] = module
512 module = self.children[modules[0]]
513 newfile = module.add_nested_file(modules[1:], path)
516 def create_node(self):
518 for childfile in self.files:
519 node.add_child(childfile.get_node())
520 for childmodule in self.children.itervalues():
521 node.add_child(childmodule.create_node())
524 def node_properties(self):
525 properties = 'label="{0}", shape=ellipse'.format(self.name)
526 properties += ', URL="\\ref module_{0}"'.format(self.name)
530 class Dependencies(object):
531 def __init__(self, rootdir, includedirs, installedfiles):
533 self.root = Module("")
536 self.rootpath.append(os.path.abspath(root))
538 self.includedirs = self.rootpath + includedirs
540 self.includedirs = self.rootpath
541 self.installedfiles = installedfiles
543 def add_file(self, filename, reporter):
544 fullpath = os.path.abspath(filename)
545 for root in self.rootpath:
546 if fullpath.startswith(root):
547 relpath = fullpath[len(root)+1:]
550 reporter.input_warning(filename,
551 'input file not under root path, skipped')
553 modules = relpath.split(os.sep)
554 newfile = self.root.add_nested_file(modules, relpath)
555 if fullpath in self.installedfiles:
556 newfile.set_installed(reporter)
557 self.files[os.path.abspath(filename)] = newfile
559 def scan_files(self, ignorelist, reporter):
560 for (filename, scanfile) in self.files.iteritems():
561 scanfile.scan(filename, self.files, self.includedirs, ignorelist,
563 for scanfile in self.files.itervalues():
564 for link in scanfile.get_links():
565 link.refresh_type(reporter)
567 def create_file_graph(self):
568 rootnode = self.root.create_node()
571 for scanfile in self.files.itervalues():
572 links.extend(scanfile.get_links())
573 graph = Graph([rootnode], links)
576 def create_modules_graph(self):
577 rootnode = self.root.create_node()
580 for scanfile in self.files.itervalues():
581 links.extend(scanfile.get_links())
582 graph = Graph([rootnode], links)
583 for node in rootnode.get_children():
584 if node.obj.name == 'gromacs':
585 for child in node.get_children():
586 graph.collapse_node(child)
588 graph.collapse_node(node)
589 graph.set_options(concentrate=False)
592 def create_module_file_graph(self, module):
593 rootnode = module.create_node()
596 for scanfile in self.files.itervalues():
597 links.extend(scanfile.get_links())
598 graph = Graph([rootnode], links)
602 def get_toplevel_modules(self):
604 for module in self.root.children.itervalues():
605 if module.name == 'gromacs':
606 result.extend(module.children.itervalues())
608 result.append(module)
612 def print_module_graph(outfile, deps, options):
613 graph = deps.create_modules_graph()
616 def print_file_graph(outfile, deps, options):
617 graph = deps.create_file_graph()
618 graph.set_options(left_to_right=options.left_to_right)
620 #if options.source_at_top:
622 # for file in deps.files.itervalues():
623 # if file.sourcefile:
624 # sourcenodes.append(file.nodename)
626 # outfile.write(' { rank = min; ' + '; '.join(sourcenodes) + '}\n')
627 #if options.with_external and options.external_at_bottom:
629 # for file in deps.files.itervalues():
630 # if not file.module:
631 # extnodes.append(file.nodename)
633 # outfile.write(' { rank = max; ' + '; '.join(extnodes) + '}\n')
635 def print_module_file_graph(outfile, deps, module, options):
636 graph = deps.create_module_file_graph(module)
637 graph.set_options(left_to_right=options.left_to_right)
641 parser = OptionParser()
642 parser.add_option('-f', '--files',
643 help='Read list of input files from given file')
644 parser.add_option('--installed',
645 help='Read list of installed files from given file')
646 parser.add_option('-R', '--rootdir', action='append',
647 help='Remove this prefix from all files')
648 parser.add_option('-I', '--includedir', action='append',
649 help='Specify additional directories to search for '
651 parser.add_option('-o', '--outdir', default='.',
652 help='Specify output directory for graphs')
653 #parser.add_option('--source-at-top', action='store_true',
654 # help='Force source files at the top of the graph')
655 #parser.add_option('--with-external', action='store_true',
656 # help='Include external dependencies in the graph')
657 #parser.add_option('--external-at-bottom', action='store_true',
658 # help='Force external dependencies files at the bottom '
660 parser.add_option('--left-to-right', action='store_true',
661 help='Lay out from left to right')
662 parser.add_option('--file-graph',
663 help='Write graph for individual files')
664 parser.add_option('--module-graph',
665 help='Write graph for modules')
666 parser.add_option('--module-file-graphs', action='store_true',
667 help='Write file graphs for each module')
668 options, args = parser.parse_args()
670 # Constructs lists of files
675 with open(options.files, 'r') as outfile:
677 if line.startswith('!'):
678 ignorelist.append(os.path.abspath(line[1:].strip()))
680 filelist.append(line.strip())
681 filelist.extend(args)
682 if options.installed:
683 with open(options.installed, 'r') as outfile:
685 installedlist.append(line.strip())
687 # Creates objects for all files and modules
688 reporter = ErrorReporter()
689 deps = Dependencies(options.rootdir, options.includedir, installedlist)
690 for filename in filelist:
691 deps.add_file(filename, reporter)
693 deps.scan_files(ignorelist, reporter)
695 #if options.with_external:
696 # for filename in extrafiles:
697 # file = files[filename]
698 # if os.path.exists(filename):
699 # with open(filename, 'r') as outfile:
700 # for line in outfile:
702 # if line.startswith(' * \inpublicapi'):
703 # file.api = "public"
704 # elif line.startswith(' * \inlibraryapi'):
705 # file.api = "library"
707 # Prints out the graph
708 if options.module_graph:
709 graphpath = os.path.join(options.outdir, options.module_graph)
710 with open(graphpath, 'w') as outfile:
711 print_module_graph(outfile, deps, options)
712 if options.file_graph:
713 graphpath = os.path.join(options.outdir, options.file_graph)
714 with open(graphpath, 'w') as outfile:
715 print_file_graph(outfile, deps, options)
716 if options.module_file_graphs:
717 options.left_to_right = True
718 for module in deps.get_toplevel_modules():
719 filename = 'module_{0}-deps.dot'.format(module.name)
720 with open(os.path.join(options.outdir, filename), 'w') as outfile:
721 print_module_file_graph(outfile, deps, module, options)