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)
101 EdgeType.undocumented = EdgeType(7)
105 """Graph edge between two Node objects in 'dot' graph.
107 Signifies an include dependency between the two nodes, and manages types
108 associated with the dependencies.
111 def __init__(self, fromnode, tonode, edgetype):
112 """Create edge between given Nodes with given type."""
113 self._fromnode = fromnode
114 self._tonode = tonode
115 self._edgetype = edgetype
117 def merge_edge(self, other):
118 """Merge another edge into this one and choose an appropriate type.
120 Updates the type of this edge based on the types of the merged edges.
122 self._edgetype = max(self._edgetype, other._edgetype)
125 """Format this edge for 'dot'."""
126 if self._fromnode.is_file_node() and self._tonode.is_file_node():
128 elif self._edgetype == EdgeType.intramodule:
130 elif self._edgetype == EdgeType.test:
131 properties = 'color=".33 .8 .8", style=dashed'
132 elif self._edgetype == EdgeType.libimpl:
133 properties = 'color=".66 .8 .8", style=dashed'
134 elif self._edgetype == EdgeType.pubimpl:
135 properties = 'color=black, style=dashed'
136 elif self._edgetype == EdgeType.library:
137 properties = 'color=".66 .8 .8"'
138 elif self._edgetype == EdgeType.public:
139 properties = 'color=black'
140 elif self._edgetype == EdgeType.legacy:
141 properties = 'color=grey75'
143 properties = 'color=red'
144 return '{0} -> {1} [{2}]'.format(self._fromnode.get_nodename(),
145 self._tonode.get_nodename(),
150 """Node in 'dot' graph."""
152 def __init__(self, nodename, label, style=None, properties=None, is_file=False):
153 """Create node with given attributes.
155 is_file does not affect the appearance of the node, but is used for
156 formatting edges between two files differently from other edges.
157 style and properties should be iterables with graphviz attributes for
160 Node can have child nodes. Such nodes are rendered as cluster
163 self._nodename = nodename
166 self._style = ','.join(style)
170 self._properties = ', '.join(properties)
172 self._properties = None
173 self._is_file = is_file
176 def add_child(self, child):
177 """Add a child node."""
178 self._children.append(child)
180 def clear_children(self):
181 """Remove all children from the node."""
184 def is_file_node(self):
185 """Return True if the node was created with is_file=True."""
188 def get_nodename(self):
189 """Get internal name of the node in 'dot'."""
190 return self._nodename
192 def get_children(self, recursive=False):
193 """Get list of child nodes."""
195 result = list(self._children)
196 for child in self._children:
197 result.extend(child.get_children(recursive=True))
200 return self._children
203 """Format this node for 'dot'."""
204 # TODO: Take indent as a parameter to make output marginally nicer.
207 result += ' subgraph cluster_{0} {{\n' \
208 .format(self._nodename)
209 result += ' label = "{0}"\n'.format(self._label)
210 for child in self._children:
211 result += child.format()
214 properties = 'label="{0}"'.format(self._label)
216 properties += ', ' + self._properties
218 properties += ', style="{0}"'.format(self._style)
219 result += ' {0} [{1}]\n'.format(self._nodename, properties)
225 """Graph for 'dot'."""
227 def __init__(self, nodes, edges):
228 """Create graph with given nodes and edges."""
229 self._nodes = set(nodes)
231 self._left_to_right = False
232 self._concentrate = True
234 def set_options(self, left_to_right=None, concentrate=None):
235 """Set output options for the graph."""
236 if left_to_right != None:
237 self._left_to_right = left_to_right
238 if concentrate != None:
239 self._concentrate = concentrate
241 def merge_nodes(self, nodes, target):
242 """Merge a set of nodes into a single node.
244 All nodes from the list nodes are merged into the target node.
245 All edges to or from the merged nodes are rerouted to/from target
246 instead. Duplicate edges are not created. Instead, if an edge already
247 exists, the edge types are merged. All nodes from the list nodes are
248 removed from the graph after the merge is done.
255 for edge in self._edges:
256 isfrom = (edge._fromnode in nodes)
257 isto = (edge._tonode in nodes)
261 if not edge._tonode in edgesfrom:
262 edgesfrom[edge._tonode] = \
263 Edge(target, edge._tonode, edge._edgetype)
265 edgesfrom[edge._tonode].merge_edge(edge)
267 if not edge._fromnode in edgesto:
268 edgesto[edge._fromnode] = \
269 Edge(edge._fromnode, target, edge._edgetype)
271 edgesto[edge._fromnode].merge_edge(edge)
273 newedges.append(edge)
274 newedges.extend(edgesfrom.values())
275 newedges.extend(edgesto.values())
276 self._edges = newedges
278 def collapse_node(self, node):
279 """Merge all children of a node into the node.
281 All child nodes are removed after the merge is done.
283 nodes = node.get_children(recursive=True)
284 self.merge_nodes(nodes, node)
285 node.clear_children()
287 def write(self, outfile):
288 """Write the graph in 'dot' format."""
289 outfile.write('digraph includedeps {\n')
290 if self._left_to_right:
291 outfile.write(' rankdir = LR\n')
292 if self._concentrate:
293 outfile.write(' concentrate = true\n')
294 outfile.write(' node [fontname="FreeSans",fontsize=10,height=.2,'
296 for node in self._nodes:
297 outfile.write(node.format())
298 for edge in self._edges:
299 outfile.write(' ' + edge.format() + '\n')
302 class GraphBuilder(object):
304 """Builder for Graph objects from gmxtree.GromacsTree representation."""
306 def __init__(self, tree):
307 """Initialize builder for a given tree representation."""
310 def _create_file_node(self, fileobj, filenodes):
311 """Create graph node for a file object.
313 filenodes is a dict() that maps file objects to their nodes, and is
314 updated by this call.
316 nodename = re.subn(r'[-./]', '_', fileobj.get_relpath())[0]
319 properties.append('URL="\\ref {0}"'.format(fileobj.get_name()))
320 if not fileobj.get_module():
322 properties.append('color=red')
323 if fileobj.is_test_file():
324 style.append('filled')
325 properties.append('fillcolor=".33 .2 1"')
326 elif fileobj.is_source_file():
327 style.append('filled')
328 properties.append('fillcolor=grey75')
329 elif fileobj.get_api_type() == DocType.public:
330 style.append('filled')
331 properties.append('fillcolor=".66 .2 1"')
332 elif fileobj.get_api_type() == DocType.library:
333 style.append('filled')
334 properties.append('fillcolor=".66 .5 1"')
335 node = Node(nodename, fileobj.get_name(), style, properties, is_file=True)
336 filenodes[fileobj] = node
339 def _create_file_edge(self, fromfile, tofile, filenodes):
340 """Create edge between two file objects.
342 Determines the type for the edge from the information provided by
345 intramodule = (fromfile.get_module() == tofile.get_module())
346 is_legacy = not tofile.get_module().is_documented()
347 if fromfile.get_module() == tofile.get_module():
348 edgetype = EdgeType.intramodule
349 elif tofile.get_api_type() == DocType.internal:
351 edgetype = EdgeType.legacy
353 edgetype = EdgeType.undocumented
354 elif fromfile.is_test_file():
355 edgetype = EdgeType.test
356 elif tofile.is_test_file():
357 edgetype = EdgeType.undocumented
358 elif fromfile.is_source_file() or \
359 (fromfile.get_api_type() <= DocType.internal and \
360 not fromfile.is_installed()):
361 if tofile.get_api_type() == DocType.public:
362 edgetype = EdgeType.pubimpl
363 elif tofile.get_api_type() == DocType.library:
364 edgetype = EdgeType.libimpl
365 elif is_legacy or not tofile.is_documented():
366 edgetype = EdgeType.legacy
368 raise ValueError('Unknown edge type between {0} and {1}'
369 .format(fromfile.path, tofile.path))
370 elif fromfile.get_api_type() == DocType.library:
371 edgetype = EdgeType.library
372 elif fromfile.get_api_type() == DocType.public or fromfile.is_installed():
373 if tofile.get_api_type() == DocType.public or \
374 tofile.get_documentation_type() == DocType.public or \
375 (tofile.is_installed() and not tofile.is_documented()):
376 edgetype = EdgeType.public
378 edgetype = EdgeType.undocumented
380 raise ValueError('Unknown edge type between {0} and {1}'
381 .format(fromfile.path, tofile.path))
382 return Edge(filenodes[fromfile], filenodes[tofile], edgetype)
384 def _create_file_edges(self, filenodes):
385 """Create edges between all file nodes.
387 Create edges between file nodes specified in filenodes from all include
388 dependencies. An edge is created only if both ends of the dependency
389 are in the list of nodes.
392 for fileobj in filenodes.iterkeys():
393 for includedfile in fileobj.get_includes():
394 otherfile = includedfile.get_file()
395 if otherfile and otherfile in filenodes:
396 edge = self._create_file_edge(fileobj, otherfile, filenodes)
400 def _create_module_node(self, module, filenodes):
401 """Create node for a module.
403 The created node will have all files in the module as its child nodes.
404 All created file nodes are added to the filenodes dict.
408 properties.append('shape=ellipse')
409 properties.append('URL="\\ref module_{0}"'.format(module.get_name()))
410 if not module.is_documented():
411 style.append('filled')
412 properties.append('fillcolor=grey75')
413 elif module.get_group() == 'analysismodules':
414 style.append('filled')
415 properties.append('fillcolor="0 .2 1"')
416 elif module.get_group() == 'utilitymodules':
417 style.append('filled')
418 properties.append('fillcolor=".08 .2 1"')
419 elif module.get_group() == 'mdrun':
420 style.append('filled')
421 properties.append('fillcolor=".75 .2 1"')
422 rootdir = module.get_root_dir()
423 if rootdir.has_installed_files():
424 properties.append('color=".66 .5 1"')
425 properties.append('penwidth=3')
426 nodename = 'module_' + re.subn(r'[-./]', '_', rootdir.get_relpath())[0]
427 label = module.get_name()[7:]
428 node = Node(nodename, label, style, properties)
429 for childfile in module.get_files():
430 node.add_child(self._create_file_node(childfile, filenodes))
433 def create_modules_graph(self):
434 """Create module dependency graph."""
438 libgromacsnode = Node('libgromacs', 'libgromacs')
439 nodes.append(libgromacsnode)
440 for moduleobj in self._tree.get_modules():
441 node = self._create_module_node(moduleobj, filenodes)
442 if moduleobj.get_root_dir().get_relpath().startswith('src/gromacs'):
443 libgromacsnode.add_child(node)
446 modulenodes.append(node)
447 edges = self._create_file_edges(filenodes)
448 graph = Graph(nodes, edges)
449 for node in modulenodes:
450 graph.collapse_node(node)
451 graph.set_options(concentrate=False)
454 def create_module_file_graph(self, module):
455 """Create file dependency graph for files within a module."""
458 for fileobj in module.get_files():
459 nodes.append(self._create_file_node(fileobj, filenodes))
460 edges = self._create_file_edges(filenodes)
461 graph = Graph(nodes, edges)
462 graph.set_options(left_to_right=True)
466 """Run the graph generation script."""
470 from optparse import OptionParser
472 from gmxtree import GromacsTree
473 from reporter import Reporter
475 parser = OptionParser()
476 parser.add_option('-S', '--source-root',
477 help='Source tree root directory')
478 parser.add_option('-B', '--build-root',
479 help='Build tree root directory')
480 parser.add_option('--installed',
481 help='Read list of installed files from given file')
482 parser.add_option('-o', '--outdir', default='.',
483 help='Specify output directory for graphs')
484 parser.add_option('-q', '--quiet', action='store_true',
485 help='Do not write status messages')
486 options, args = parser.parse_args()
489 if options.installed:
490 with open(options.installed, 'r') as outfile:
492 installedlist.append(line.strip())
494 reporter = Reporter(quiet=True)
496 if not options.quiet:
497 sys.stderr.write('Scanning source tree...\n')
498 tree = GromacsTree(options.source_root, options.build_root, reporter)
499 tree.set_installed_file_list(installedlist)
500 if not options.quiet:
501 sys.stderr.write('Reading source files...\n')
503 if not options.quiet:
504 sys.stderr.write('Reading Doxygen XML files...\n')
505 tree.load_xml(only_files=True)
507 if not options.quiet:
508 sys.stderr.write('Writing graphs...\n')
509 graphbuilder = GraphBuilder(tree)
510 if not os.path.exists(options.outdir):
511 os.mkdir(options.outdir)
513 filename = os.path.join(options.outdir, 'module-deps.dot')
514 graph = graphbuilder.create_modules_graph()
515 with open(filename, 'w') as outfile:
518 # Skip some modules that are too big to make any sense
519 skippedmodules = ('legacyheaders', 'gmxlib', 'mdlib', 'gmxana', 'gmxpreprocess')
520 for module in tree.get_modules():
521 if not module.get_name()[7:] in skippedmodules:
522 filename = '{0}-deps.dot'.format(module.get_name())
523 filename = os.path.join(options.outdir, filename)
524 graph = graphbuilder.create_module_file_graph(module)
525 with open(filename, 'w') as outfile:
528 if __name__ == '__main__':