3 # This file is part of the GROMACS molecular simulation package.
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.
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 """Check and generate include dependency graphs for Gromacs.
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'.
49 The legend for per-file graph nodes:
51 light blue: public headers
52 dark blue: library headers
53 no background: other files
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.
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
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
77 from optparse import OptionParser
79 class ErrorReporter(object):
80 def input_warning(self, file_path, msg):
81 sys.stderr.write('warning: {0}: {1}\n'.format(file_path, msg))
83 def error(self, file_path, msg):
84 sys.stderr.write('error: {0}: {1}\n'.format(file_path, msg))
88 """Link between two node objects.
90 Signifies an include dependency between the two nodes, and manages types
91 associated with the dependencies.
104 def __init__(self, fromnode, tonode, link_type):
105 self.fromnode = fromnode
107 self.link_type = link_type
108 if link_type not in Link._priorities:
109 raise ValueError('Unknown link type {0}'.format(link_type))
111 def merge_link(self, other):
112 """Merge another link into this one and choose an appropriate type.
114 Updates the type of this link based on the types of the merged links.
116 if Link._priorities[other.link_type] < Link._priorities[self.link_type]:
117 self.link_type = other.link_type
120 """Format this link for 'dot'."""
121 if self.fromnode.is_file_node() and self.tonode.is_file_node():
123 elif self.link_type == 'intramodule':
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'
138 properties = 'color=red'
139 return '{0} -> {1} [{2}]'.format(self.fromnode.nodename,
140 self.tonode.nodename,
144 def __init__(self, nodename, label, properties, is_file):
145 self.nodename = nodename
147 self._properties = properties
148 self._is_file = is_file
155 def add_child(self, child):
156 self.children.append(child)
158 def remove_child(self, child):
159 self.children.remove(child)
161 def clear_children(self):
164 def is_file_node(self):
167 def get_children(self, recursive=False):
169 result = list(self.children)
170 for child in self.children:
171 result.extend(child.get_children(recursive=True))
177 """Format this node for 'dot'."""
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()
189 properties = 'label="{0}"'.format(self.label)
191 properties += ', ' + self._properties
192 result += ' {0} [{1}]\n'.format(self.nodename, properties)
197 def __init__(self, nodes, links):
198 self.nodes = set(nodes)
200 self.left_to_right = False
201 self.concentrate = True
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
209 def prune_links(self):
211 for node in self.nodes:
212 nodes.update(node.get_children(recursive=True))
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
219 def merge_nodes(self, nodes, target):
225 for link in self.links:
226 isfrom = (link.fromnode in nodes)
227 isto = (link.tonode in nodes)
231 if not link.tonode in linksfrom:
232 linksfrom[link.tonode] = \
233 Link(target, link.tonode, link.link_type)
235 linksfrom[link.tonode].merge_link(link)
237 if not link.fromnode in linksto:
238 linksto[link.fromnode] = \
239 Link(link.fromnode, target, link.link_type)
241 linksto[link.fromnode].merge_link(link)
243 newlinks.append(link)
244 newlinks.extend(linksfrom.values())
245 newlinks.extend(linksto.values())
246 self.links = newlinks
248 def collapse_node(self, node):
249 nodes = node.get_children(recursive=True)
250 self.merge_nodes(nodes, node)
251 node.clear_children()
253 def write(self, outfile):
254 outfile.write('digraph includedeps {\n')
255 if self.left_to_right:
256 outfile.write(' rankdir = LR\n')
258 outfile.write(' concentrate = true\n')
259 outfile.write(' node [fontname="FreeSans",fontsize=10,height=.2,'
261 for link in self.links:
262 outfile.write(' ' + link.format() + '\n')
263 for node in self.nodes:
264 outfile.write(node.format())
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):
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
287 def __init__(self, path, module):
289 self.name = os.path.basename(path)
291 if module.name == 'tests' or module.name == 'legacytests':
293 elif re.search(r'\.c(pp|u)?$', self.name) != None:
297 self.doctype = 'none'
298 #headername = re.sub(r'\.cpp$', '.h', self.name)
299 #implheadername = re.sub(r'\.cpp$', '-impl.h', self.name)
301 self.installed = False
303 def is_documented(self):
304 return self.doctype != 'none'
306 def is_installed(self):
307 return self.installed
309 def set_installed(self, reporter):
310 if self.type != 'header':
311 reporter.input_warning(self.path,
312 'installing {0} file'.format(self.type))
314 self.installed = True
316 def get_included_files(self):
317 return self._included
319 def scan_include_file(self, line, allfiles, selfdir, includedirs,
320 ignorelist, reporter):
321 """Process #include directive during scan().
323 Searches for the included file in given directories, does some checks,
324 and adds the dependency link to the other file if applicable.
331 match = re.match(r'#include *<([^>]*)>', line)
333 includedpath = match.group(1)
335 fullpath = find_include_file(includedpath, includedirs)
337 match = re.match(r'#include *"([^"]*)"', line)
339 includedpath = match.group(1)
340 fullpath = os.path.abspath(os.path.join(selfdir, includedpath))
341 #if os.path.abspath(fullpath) in ignorelist:
343 if os.path.exists(fullpath):
346 fullpath = find_include_file(includedpath, includedirs)
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))
353 reporter.input_warning(self.path, 'line "{0}" could not be parsed'
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))
366 def scan(self, filename, allfiles, includedirs, ignorelist, reporter):
367 selfdir = os.path.dirname(filename)
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)
377 if not foundfileblock:
381 foundfileblock = True
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)
391 reporter.error(self.path,
392 'file documented in multiple modules')
393 self.docmodule = match.group(1)
395 match = re.match(r'/\*! *(\\[a-z]*internal)? *\\file', line)
397 docspec = match.group(1)
399 self.doctype = 'public'
400 elif docspec == r'\libinternal':
401 self.doctype = 'library'
402 elif docspec == r'\internal':
403 self.doctype = 'implementation'
405 reporter.input_warning(self.path,
406 'unknown specifier "{0}"'.format(docspec))
407 self.doctype = 'unknown'
409 if self.type == 'header':
410 # Default type if no other found
411 self.type = 'implheader'
414 class Module(object):
415 def __init__(self, name, parent = None):
419 self.fullname = parent.fullname + '_' + name
421 self.fullname = 'module'
423 self.children = dict()
424 self.is_top_level = (not parent or parent.name in ('', 'gromacs'))
426 def get_parent(self):
429 def is_child(self, module):
430 parent = module.parent
434 parent = parent.parent
437 def get_top_level_module(self):
438 if self.is_top_level or not self.parent:
440 return self.parent.get_top_level_module()
442 def add_nested_file(self, modules, path):
443 if len(modules) == 1:
444 newfile = File(path, self)
445 self.files.append(newfile)
447 if not modules[0] in self.children:
448 module = Module(modules[0], self)
449 self.children[modules[0]] = module
451 module = self.children[modules[0]]
452 newfile = module.add_nested_file(modules[1:], path)
456 class Dependencies(object):
457 def __init__(self, rootdir, includedirs, installedfiles):
459 self.root = Module("")
462 self.rootpath.append(os.path.abspath(root))
464 self.includedirs = self.rootpath + includedirs
466 self.includedirs = self.rootpath
467 self.installedfiles = installedfiles
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:]
476 reporter.input_warning(filename,
477 'input file not under root path, skipped')
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
485 def scan_files(self, ignorelist, reporter):
486 for (filename, scanfile) in self.files.iteritems():
487 scanfile.scan(filename, self.files, self.includedirs, ignorelist,
490 def get_toplevel_modules(self):
492 for module in self.root.children.itervalues():
493 if module.name == 'gromacs':
494 result.extend(module.children.itervalues())
496 result.append(module)
500 def _is_legacy_module(module):
501 if module.name in ('legacyheaders', 'gmxlib', 'mdlib', 'gmxana', 'gmxpreprocess'):
503 if module.get_parent():
504 return _is_legacy_module(module.get_parent())
508 class IncludeFileChecker(object):
509 def __init__(self, deps, options):
511 self._options = options
513 def _check_file(self, checkfile, reporter):
514 if not self._options.check_doc:
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
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}"'
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>.
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))
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():
570 if not self._options.check_doc:
573 (checkfile.module.get_top_level_module() == \
574 otherfile.module.get_top_level_module())
575 if otherfile.type not in ('publicheader', 'libheader'):
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))
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)
593 class GraphBuilder(object):
594 def __init__(self, deps):
597 def _create_file_node(self, fileobj, filenodes):
598 nodename = re.subn(r'[-./]', '_', fileobj.path)[0]
601 properties.append('URL="\\ref {0}"'.format(fileobj.name))
602 if not fileobj.module:
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"')
615 properties.append('style="{0}"'.format(','.join(style)))
616 node = Node(nodename, fileobj.name, ', '.join(properties), is_file=True)
617 filenodes[fileobj] = node
620 def _create_file_edge(self, fromfile, tofile, filenodes):
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'):
627 link_type = 'intramodule'
631 link_type = 'undocumented'
632 elif fromfile.type == 'test':
634 elif fromfile.type in ('source', 'header', 'implheader') and \
635 not fromfile.is_installed():
637 link_type = 'intramodule'
638 elif tofile.type == 'publicheader':
639 link_type = 'pubimpl'
640 elif tofile.type == 'libheader':
641 link_type = 'libimpl'
644 elif not tofile.is_documented():
647 raise ValueError('Unknown link type between {0} and {1}'
648 .format(fromfile.path, tofile.path))
649 elif fromfile.type == 'libheader':
650 link_type = 'library'
651 elif fromfile.type == 'publicheader' or fromfile.is_installed():
652 if tofile.type == 'publicheader' or tofile.doctype == 'public' or \
653 (tofile.is_installed() and not tofile.is_documented()):
656 link_type = 'undocumented'
658 raise ValueError('Unknown link type between {0} and {1}'
659 .format(fromfile.path, tofile.path))
660 return Link(filenodes[fromfile], filenodes[tofile], link_type)
662 def _create_file_edges(self, fileobj, filenodes):
664 if fileobj in filenodes:
665 for includedfile in fileobj.get_included_files():
666 otherfile = includedfile._included_file
667 if otherfile and otherfile in filenodes:
668 link = self._create_file_edge(fileobj, otherfile, filenodes)
672 def create_module_node(self, module, filenodes):
673 properties = 'shape=ellipse, URL="\\ref module_{0}"'.format(module.name)
674 if _is_legacy_module(module):
675 properties += 'style=filled, fillcolor=grey75'
676 node = Node(module.fullname, module.name, properties, is_file=False)
677 for childfile in module.files:
678 node.add_child(self._create_file_node(childfile, filenodes))
679 for childmodule in module.children.itervalues():
680 node.add_child(self.create_module_node(childmodule, filenodes))
683 def create_file_graph(self):
685 rootnode = self.create_module_node(self._deps.root, filenodes)
688 for scanfile in self._deps.files.itervalues():
689 links.extend(self._create_file_edges(scanfile, filenodes))
690 graph = Graph([rootnode], links)
693 def create_modules_graph(self):
695 rootnode = self.create_module_node(self._deps.root, filenodes)
698 for scanfile in self._deps.files.itervalues():
699 links.extend(self._create_file_edges(scanfile, filenodes))
700 graph = Graph([rootnode], links)
701 for node in rootnode.get_children():
702 if node.label == 'gromacs':
705 for child in node.get_children():
706 if child.is_file_node():
707 header_nodes.append(child)
709 graph.collapse_node(child)
710 module_nodes.append(child)
711 for header in header_nodes:
712 for module in module_nodes:
713 if header.nodename.startswith(module.nodename[7:]):
714 # graph.merge_nodes([header], module)
715 node.remove_child(header)
718 graph.collapse_node(node)
719 graph.set_options(concentrate=False)
723 def create_module_file_graph(self, module):
725 rootnode = self.create_module_node(module, filenodes)
728 for scanfile in self._deps.files.itervalues():
729 links.extend(self._create_file_edges(scanfile, filenodes))
730 graph = Graph([rootnode], links)
735 def print_module_graph(outfile, graphbuilder, options):
736 graph = graphbuilder.create_modules_graph()
739 def print_file_graph(outfile, graphbuilder, options):
740 graph = graphbuilder.create_file_graph()
741 graph.set_options(left_to_right=options.left_to_right)
743 #if options.source_at_top:
745 # for file in deps.files.itervalues():
746 # if file.sourcefile:
747 # sourcenodes.append(file.nodename)
749 # outfile.write(' { rank = min; ' + '; '.join(sourcenodes) + '}\n')
750 #if options.with_external and options.external_at_bottom:
752 # for file in deps.files.itervalues():
753 # if not file.module:
754 # extnodes.append(file.nodename)
756 # outfile.write(' { rank = max; ' + '; '.join(extnodes) + '}\n')
758 def print_module_file_graph(outfile, graphbuilder, module, options):
759 graph = graphbuilder.create_module_file_graph(module)
760 graph.set_options(left_to_right=options.left_to_right)
764 parser = OptionParser()
765 parser.add_option('-f', '--files',
766 help='Read list of input files from given file')
767 parser.add_option('--installed',
768 help='Read list of installed files from given file')
769 parser.add_option('-R', '--rootdir', action='append',
770 help='Remove this prefix from all files')
771 parser.add_option('-I', '--includedir', action='append',
772 help='Specify additional directories to search for '
774 parser.add_option('-o', '--outdir', default='.',
775 help='Specify output directory for graphs')
776 #parser.add_option('--source-at-top', action='store_true',
777 # help='Force source files at the top of the graph')
778 #parser.add_option('--with-external', action='store_true',
779 # help='Include external dependencies in the graph')
780 #parser.add_option('--external-at-bottom', action='store_true',
781 # help='Force external dependencies files at the bottom '
783 parser.add_option('--check', action='store_true',
784 help='Check for problems in include file dependencies')
785 parser.add_option('--check-doc', action='store_true',
786 help='Check for problems in Doxygen documentation')
787 parser.add_option('--warn-undoc', action='store_true',
788 help='Warn for files that do not have Doxygen documentation')
789 parser.add_option('--left-to-right', action='store_true',
790 help='Lay out from left to right')
791 parser.add_option('--file-graph',
792 help='Write graph for individual files')
793 parser.add_option('--module-graph',
794 help='Write graph for modules')
795 parser.add_option('--module-file-graphs', action='store_true',
796 help='Write file graphs for each module')
797 options, args = parser.parse_args()
799 if not options.file_graph and not options.module_graph and \
800 not options.module_file_graphs:
803 # Constructs lists of files
808 with open(options.files, 'r') as outfile:
810 if line.startswith('!'):
811 ignorelist.append(os.path.abspath(line[1:].strip()))
813 filelist.append(line.strip())
814 filelist.extend(args)
815 if options.installed:
816 with open(options.installed, 'r') as outfile:
818 installedlist.append(line.strip())
820 # Creates objects for all files and modules
821 reporter = ErrorReporter()
822 deps = Dependencies(options.rootdir, options.includedir, installedlist)
823 for filename in filelist:
824 deps.add_file(filename, reporter)
826 deps.scan_files(ignorelist, reporter)
828 if options.check or options.check_doc:
829 checker = IncludeFileChecker(deps, options)
830 checker.check_all(reporter)
832 #if options.with_external:
833 # for filename in extrafiles:
834 # file = files[filename]
835 # if os.path.exists(filename):
836 # with open(filename, 'r') as outfile:
837 # for line in outfile:
839 # if line.startswith(' * \inpublicapi'):
840 # file.api = "public"
841 # elif line.startswith(' * \inlibraryapi'):
842 # file.api = "library"
844 # Prints out the graph
845 graphbuilder = GraphBuilder(deps)
846 if options.module_graph:
847 graphpath = os.path.join(options.outdir, options.module_graph)
848 with open(graphpath, 'w') as outfile:
849 print_module_graph(outfile, graphbuilder, options)
850 if options.file_graph:
851 graphpath = os.path.join(options.outdir, options.file_graph)
852 with open(graphpath, 'w') as outfile:
853 print_file_graph(outfile, graphbuilder, options)
854 if options.module_file_graphs:
855 options.left_to_right = True
856 for module in deps.get_toplevel_modules():
857 if not _is_legacy_module(module):
858 filename = 'module_{0}-deps.dot'.format(module.name)
859 filename = os.path.join(options.outdir, filename)
860 with open(filename, 'w') as outfile:
861 print_module_file_graph(outfile, graphbuilder, module, options)