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 """Generate include dependency graphs.
38 This script generates include dependency graphs from the GROMACS source tree.
39 One graph is generated to show inter-module dependencies, and separate graphs
40 for each module to show file-level dependencies within the module.
42 Output format for the graphs is suitable for processing with 'dot' in graphviz.
44 The graphs are built from the source tree representation constructed in
47 Classes Graph, Node, Edge, and EdgeType provide a relatively general
48 implementation for constructing 'dot' graphs. GraphBuilder is used to
49 create Graph instances from a gmxtree.GromacsTree object; the actual graph
50 objects will not contain any references to the gmxtree objects.
52 When run in script mode, the GromacsTree object is first constructed, and then
53 GraphBuilder is used to construct the necessary graphs, which are then written
56 The produced graphs are documented in doxygen.md.
62 from gmxtree import DocType
64 class EdgeType(object):
66 """Enumeration type for edge types in include dependency graphs."""
68 # Mapping to string representation for the internal integer values
69 _names = ['test', 'pubimpl', 'libimpl', 'library', 'public',
70 'intramodule', 'legacy', 'undocumented']
72 def __init__(self, value):
73 """Initialize a EdgeType instance.
75 EdgeType.{test,pubimpl,...,undocumented} should be used outside the
76 class instead of calling the constructor.
81 """Return string representation for the edge type (for debugging)."""
82 return self._names[self._value]
84 def __cmp__(self, other):
85 """Order edge types in the order of increasing coupling."""
86 return cmp(self._value, other._value)
88 # Tests depend on test
89 EdgeType.test = EdgeType(0)
90 # Implementation depends on public/library headers
91 EdgeType.pubimpl = EdgeType(1)
92 EdgeType.libimpl = EdgeType(2)
93 # Library header depends on other module
94 EdgeType.library = EdgeType(3)
95 # Public header depends on other module
96 EdgeType.public = EdgeType(4)
97 # Intramodule dependency
98 EdgeType.intramodule = EdgeType(5)
99 EdgeType.legacy = EdgeType(6)
100 EdgeType.cyclic = EdgeType(7)
102 EdgeType.undocumented = EdgeType(8)
106 """Graph edge between two Node objects in 'dot' graph.
108 Signifies an include dependency between the two nodes, and manages types
109 associated with the dependencies.
112 def __init__(self, fromnode, tonode, edgetype):
113 """Create edge between given Nodes with given type."""
114 self._fromnode = fromnode
115 self._tonode = tonode
116 self._edgetype = edgetype
118 def merge_edge(self, other):
119 """Merge another edge into this one and choose an appropriate type.
121 Updates the type of this edge based on the types of the merged edges.
123 self._edgetype = max(self._edgetype, other._edgetype)
126 """Format this edge for 'dot'."""
127 # If you change these styles, update also the legend in modulegraph.md
128 if self._fromnode.is_file_node() and self._tonode.is_file_node():
130 elif self._edgetype == EdgeType.intramodule:
132 elif self._edgetype == EdgeType.test:
133 properties = 'color=".33 .8 .8", style=dashed'
134 elif self._edgetype == EdgeType.libimpl:
135 properties = 'color=".66 .8 .8", style=dashed'
136 elif self._edgetype == EdgeType.pubimpl:
137 properties = 'color=black, style=dashed'
138 elif self._edgetype == EdgeType.library:
139 properties = 'color=".66 .8 .8"'
140 elif self._edgetype == EdgeType.public:
141 properties = 'color=black'
142 elif self._edgetype == EdgeType.legacy:
143 properties = 'color=grey75'
144 elif self._edgetype == EdgeType.cyclic:
145 properties = 'color=red, constraint=no'
147 properties = 'color=red'
148 return '{0} -> {1} [{2}]'.format(self._fromnode.get_nodename(),
149 self._tonode.get_nodename(),
154 """Node in 'dot' graph."""
156 def __init__(self, nodename, label, style=None, properties=None, is_file=False):
157 """Create node with given attributes.
159 is_file does not affect the appearance of the node, but is used for
160 formatting edges between two files differently from other edges.
161 style and properties should be iterables with graphviz attributes for
164 Node can have child nodes. Such nodes are rendered as cluster
167 self._nodename = nodename
170 self._style = ','.join(style)
174 self._properties = ', '.join(properties)
176 self._properties = None
177 self._is_file = is_file
180 def add_child(self, child):
181 """Add a child node."""
182 self._children.append(child)
184 def clear_children(self):
185 """Remove all children from the node."""
188 def is_file_node(self):
189 """Return True if the node was created with is_file=True."""
192 def get_nodename(self):
193 """Get internal name of the node in 'dot'."""
194 return self._nodename
196 def get_children(self, recursive=False):
197 """Get list of child nodes."""
199 result = list(self._children)
200 for child in self._children:
201 result.extend(child.get_children(recursive=True))
204 return self._children
207 """Format this node for 'dot'."""
208 # TODO: Take indent as a parameter to make output marginally nicer.
211 result += ' subgraph cluster_{0} {{\n' \
212 .format(self._nodename)
213 result += ' label = "{0}"\n'.format(self._label)
214 for child in self._children:
215 result += child.format()
218 properties = 'label="{0}"'.format(self._label)
220 properties += ', ' + self._properties
222 properties += ', style="{0}"'.format(self._style)
223 result += ' {0} [{1}]\n'.format(self._nodename, properties)
229 """Graph for 'dot'."""
231 def __init__(self, nodes, edges):
232 """Create graph with given nodes and edges."""
233 self._nodes = set(nodes)
235 self._left_to_right = False
236 self._concentrate = True
238 def set_options(self, left_to_right=None, concentrate=None):
239 """Set output options for the graph."""
240 if left_to_right != None:
241 self._left_to_right = left_to_right
242 if concentrate != None:
243 self._concentrate = concentrate
245 def merge_nodes(self, nodes, target):
246 """Merge a set of nodes into a single node.
248 All nodes from the list nodes are merged into the target node.
249 All edges to or from the merged nodes are rerouted to/from target
250 instead. Duplicate edges are not created. Instead, if an edge already
251 exists, the edge types are merged. All nodes from the list nodes are
252 removed from the graph after the merge is done.
259 for edge in self._edges:
260 isfrom = (edge._fromnode in nodes)
261 isto = (edge._tonode in nodes)
265 if not edge._tonode in edgesfrom:
266 edgesfrom[edge._tonode] = \
267 Edge(target, edge._tonode, edge._edgetype)
269 edgesfrom[edge._tonode].merge_edge(edge)
271 if not edge._fromnode in edgesto:
272 edgesto[edge._fromnode] = \
273 Edge(edge._fromnode, target, edge._edgetype)
275 edgesto[edge._fromnode].merge_edge(edge)
277 newedges.append(edge)
278 newedges.extend(edgesfrom.values())
279 newedges.extend(edgesto.values())
280 self._edges = newedges
282 def collapse_node(self, node):
283 """Merge all children of a node into the node.
285 All child nodes are removed after the merge is done.
287 nodes = node.get_children(recursive=True)
288 self.merge_nodes(nodes, node)
289 node.clear_children()
291 def write(self, outfile):
292 """Write the graph in 'dot' format."""
293 outfile.write('digraph includedeps {\n')
294 if self._left_to_right:
295 outfile.write(' rankdir = LR\n')
296 if self._concentrate:
297 outfile.write(' concentrate = true\n')
298 outfile.write(' node [fontname="FreeSans",fontsize=10,height=.2,'
300 for node in self._nodes:
301 outfile.write(node.format())
302 for edge in self._edges:
303 outfile.write(' ' + edge.format() + '\n')
306 class GraphBuilder(object):
308 """Builder for Graph objects from gmxtree.GromacsTree representation."""
310 def __init__(self, tree):
311 """Initialize builder for a given tree representation."""
314 def _create_file_node(self, fileobj, filenodes):
315 """Create graph node for a file object.
317 filenodes is a dict() that maps file objects to their nodes, and is
318 updated by this call.
320 nodename = re.subn(r'[-./]', '_', fileobj.get_relpath())[0]
323 properties.append('URL="\\ref {0}"'.format(fileobj.get_name()))
324 if not fileobj.get_module():
326 properties.append('color=red')
327 if fileobj.is_test_file():
328 style.append('filled')
329 properties.append('fillcolor=".33 .2 1"')
330 elif fileobj.is_source_file():
331 style.append('filled')
332 properties.append('fillcolor=grey75')
333 elif fileobj.get_api_type() == DocType.public:
334 style.append('filled')
335 properties.append('fillcolor=".66 .2 1"')
336 elif fileobj.get_api_type() == DocType.library:
337 style.append('filled')
338 properties.append('fillcolor=".66 .5 1"')
339 node = Node(nodename, fileobj.get_name(), style, properties, is_file=True)
340 filenodes[fileobj] = node
343 def _get_file_edge_type(self, fromfile, tofile):
344 """Get EdgeType for an edge between two file objects.
346 Determines the type for the edge from the information provided by
349 intramodule = (fromfile.get_module() == tofile.get_module())
350 is_legacy = not tofile.api_type_is_reliable()
351 if fromfile.get_module() == tofile.get_module():
352 return EdgeType.intramodule
353 elif tofile.get_api_type() == DocType.internal and not tofile.is_public():
355 return EdgeType.legacy
357 return EdgeType.undocumented
358 elif fromfile.is_test_file():
360 elif tofile.is_test_file():
361 return EdgeType.undocumented
362 elif fromfile.is_module_internal():
363 if tofile.is_public():
364 return EdgeType.pubimpl
365 elif tofile.get_api_type() == DocType.library:
366 return EdgeType.libimpl
368 return EdgeType.legacy
369 elif not tofile.is_documented():
370 return EdgeType.undocumented
372 raise ValueError('Unknown edge type between {0} and {1}'
373 .format(fromfile.get_relpath(), tofile.get_relpath()))
374 elif fromfile.get_api_type() == DocType.library:
375 return EdgeType.library
376 elif fromfile.is_public() or fromfile.is_installed():
377 if tofile.is_public() or tofile.is_installed():
378 return EdgeType.public
380 return EdgeType.undocumented
382 return EdgeType.legacy
384 raise ValueError('Unknown edge type between {0} and {1}'
385 .format(fromfile.get_relpath(), tofile.get_relpath()))
387 def _create_file_edge(self, fromfile, tofile, filenodes):
388 """Create edge between two file objects.
390 Determines the type for the edge from the information provided by
393 edgetype = self._get_file_edge_type(fromfile, tofile)
394 return Edge(filenodes[fromfile], filenodes[tofile], edgetype)
396 def _create_file_edges(self, filenodes):
397 """Create edges between all file nodes.
399 Create edges between file nodes specified in filenodes from all include
400 dependencies. An edge is created only if both ends of the dependency
401 are in the list of nodes.
404 for fileobj in filenodes.iterkeys():
405 for includedfile in fileobj.get_includes():
406 otherfile = includedfile.get_file()
407 if otherfile and otherfile in filenodes:
408 edge = self._create_file_edge(fileobj, otherfile, filenodes)
412 def _get_module_color(self, modulegroup):
413 # If you change these styles, update also the legend in modulegraph.md
414 if modulegroup == 'legacy':
415 return 'fillcolor=grey75'
416 elif modulegroup == 'analysismodules':
417 return 'fillcolor="0 .2 1"'
418 elif modulegroup == 'utilitymodules':
419 return 'fillcolor=".08 .2 1"'
420 elif modulegroup == 'mdrun':
421 return 'fillcolor=".75 .2 1"'
424 def _create_module_node(self, module):
425 """Create node for a module."""
428 properties.append('shape=ellipse')
429 if module.is_documented():
430 properties.append('URL="\\ref {0}"'.format(module.get_name()))
431 if not module.is_documented():
432 fillcolor = self._get_module_color('legacy')
434 fillcolor = self._get_module_color(module.get_group())
436 style.append('filled')
437 properties.append(fillcolor)
438 rootdir = module.get_root_dir()
439 if rootdir.has_installed_files():
440 properties.append('color=".66 .5 1"')
441 properties.append('penwidth=3')
442 nodename = 'module_' + re.subn(r'[-./]', '_', rootdir.get_relpath())[0]
443 label = module.get_name()[7:]
444 node = Node(nodename, label, style, properties)
447 def _create_module_edges(self, modulenodes):
448 """Create edges between all module nodes.
450 Create edges between module nodes specified in modulenodes from all
451 include dependencies. An edge is created only if both ends of the
452 dependency are in the list of nodes.
455 for moduleobj in modulenodes.iterkeys():
456 for dep in moduleobj.get_dependencies():
457 othermodule = dep.get_other_module()
458 if othermodule and othermodule in modulenodes:
459 if dep.is_cycle_suppressed():
460 edgetype = EdgeType.cyclic
463 self._get_file_edge_type(x.get_including_file(), x.get_file())
464 for x in dep.get_included_files()])
465 edge = Edge(modulenodes[moduleobj], modulenodes[othermodule], edgetype)
469 def create_modules_graph(self):
470 """Create module dependency graph."""
473 libgromacsnode = Node('libgromacs', 'libgromacs')
474 nodes.append(libgromacsnode)
475 for moduleobj in self._tree.get_modules():
476 node = self._create_module_node(moduleobj)
477 if moduleobj.get_root_dir().get_relpath().startswith('src/gromacs'):
478 libgromacsnode.add_child(node)
481 modulenodes[moduleobj] = node
482 edges = self._create_module_edges(modulenodes)
483 graph = Graph(nodes, edges)
484 graph.set_options(concentrate=False)
487 def create_module_file_graph(self, module):
488 """Create file dependency graph for files within a module."""
491 for fileobj in module.get_files():
492 nodes.append(self._create_file_node(fileobj, filenodes))
493 edges = self._create_file_edges(filenodes)
494 graph = Graph(nodes, edges)
495 graph.set_options(left_to_right=True)
499 """Run the graph generation script."""
503 from optparse import OptionParser
505 from gmxtree import GromacsTree
506 from reporter import Reporter
508 parser = OptionParser()
509 parser.add_option('-S', '--source-root',
510 help='Source tree root directory')
511 parser.add_option('-B', '--build-root',
512 help='Build tree root directory')
513 parser.add_option('--ignore-cycles',
514 help='Set file with module dependencies to ignore in cycles')
515 parser.add_option('-o', '--outdir', default='.',
516 help='Specify output directory for graphs')
517 parser.add_option('-q', '--quiet', action='store_true',
518 help='Do not write status messages')
519 options, args = parser.parse_args()
521 reporter = Reporter(quiet=True)
523 if not options.quiet:
524 sys.stderr.write('Scanning source tree...\n')
525 tree = GromacsTree(options.source_root, options.build_root, reporter)
526 tree.load_installed_file_list()
527 if not options.quiet:
528 sys.stderr.write('Reading source files...\n')
530 if options.ignore_cycles:
531 tree.load_cycle_suppression_list(options.ignore_cycles)
532 if not options.quiet:
533 sys.stderr.write('Reading Doxygen XML files...\n')
534 tree.load_xml(only_files=True)
536 if not options.quiet:
537 sys.stderr.write('Writing graphs...\n')
538 graphbuilder = GraphBuilder(tree)
539 if not os.path.exists(options.outdir):
540 os.mkdir(options.outdir)
542 filename = os.path.join(options.outdir, 'module-deps.dot')
543 graph = graphbuilder.create_modules_graph()
544 with open(filename, 'w') as outfile:
547 # Skip some modules that are too big to make any sense
548 skippedmodules = ('legacyheaders', 'gmxlib', 'mdlib', 'gmxana', 'gmxpreprocess')
549 for module in tree.get_modules():
550 if not module.get_name()[7:] in skippedmodules:
551 filename = '{0}-deps.dot'.format(module.get_name())
552 filename = os.path.join(options.outdir, filename)
553 graph = graphbuilder.create_module_file_graph(module)
554 with open(filename, 'w') as outfile:
557 if __name__ == '__main__':