3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2012,2013,2014,2015,2018 by the GROMACS development team.
6 # Copyright (c) 2019,2020, by the GROMACS development team, led by
7 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
8 # and including many others, as listed in the AUTHORS file in the
9 # top-level source directory and at http://www.gromacs.org.
11 # GROMACS is free software; you can redistribute it and/or
12 # modify it under the terms of the GNU Lesser General Public License
13 # as published by the Free Software Foundation; either version 2.1
14 # of the License, or (at your option) any later version.
16 # GROMACS is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 # Lesser General Public License for more details.
21 # You should have received a copy of the GNU Lesser General Public
22 # License along with GROMACS; if not, see
23 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
24 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 # If you want to redistribute modifications to GROMACS, please
27 # consider that scientific software is very special. Version
28 # control is crucial - bugs must be traceable. We will be happy to
29 # consider code for inclusion in the official distribution, but
30 # derived work must not be called official GROMACS. Details are found
31 # in the README & COPYING files - if they are missing, get the
32 # official version at http://www.gromacs.org.
34 # To help us fund GROMACS development, we humbly ask that you cite
35 # the research papers on the package. Check out http://www.gromacs.org.
37 """Generate include dependency graphs.
39 This script generates include dependency graphs from the GROMACS source tree.
40 One graph is generated to show inter-module dependencies, and separate graphs
41 for each module to show file-level dependencies within the module.
43 Output format for the graphs is suitable for processing with 'dot' in graphviz.
45 The graphs are built from the source tree representation constructed in
48 Classes Graph, Node, Edge, and EdgeType provide a relatively general
49 implementation for constructing 'dot' graphs. GraphBuilder is used to
50 create Graph instances from a gmxtree.GromacsTree object; the actual graph
51 objects will not contain any references to the gmxtree objects.
53 When run in script mode, the GromacsTree object is first constructed, and then
54 GraphBuilder is used to construct the necessary graphs, which are then written
57 The produced graphs are documented in doxygen.md.
64 from gmxtree
import DocType
66 @functools.total_ordering
67 class EdgeType(object):
69 """Enumeration type for edge types in include dependency graphs."""
71 # Mapping to string representation for the internal integer values
72 _names
= ['test', 'pubimpl', 'libimpl', 'library', 'public',
73 'intramodule', 'legacy', 'undocumented']
75 def __init__(self
, value
):
76 """Initialize a EdgeType instance.
78 EdgeType.{test,pubimpl,...,undocumented} should be used outside the
79 class instead of calling the constructor.
84 """Return string representation for the edge type (for debugging)."""
85 return self
._names
[self
._value
]
87 def __eq__(self
, other
):
88 """Order edge types in the order of increasing coupling."""
89 return self
._value
== other
._value
91 def __lt__(self
, other
):
92 """Order edge types in the order of increasing coupling."""
93 return self
._value
< other
._value
95 # Tests depend on test
96 EdgeType
.test
= EdgeType(0)
97 # Implementation depends on public/library headers
98 EdgeType
.pubimpl
= EdgeType(1)
99 EdgeType
.libimpl
= EdgeType(2)
100 # Library header depends on other module
101 EdgeType
.library
= EdgeType(3)
102 # Public header depends on other module
103 EdgeType
.public
= EdgeType(4)
104 # Intramodule dependency
105 EdgeType
.intramodule
= EdgeType(5)
106 EdgeType
.legacy
= EdgeType(6)
107 EdgeType
.cyclic
= EdgeType(7)
109 EdgeType
.undocumented
= EdgeType(8)
113 """Graph edge between two Node objects in 'dot' graph.
115 Signifies an include dependency between the two nodes, and manages types
116 associated with the dependencies.
119 def __init__(self
, fromnode
, tonode
, edgetype
):
120 """Create edge between given Nodes with given type."""
121 self
._fromnode
= fromnode
122 self
._tonode
= tonode
123 self
._edgetype
= edgetype
125 def merge_edge(self
, other
):
126 """Merge another edge into this one and choose an appropriate type.
128 Updates the type of this edge based on the types of the merged edges.
130 self
._edgetype
= max(self
._edgetype
, other
._edgetype
)
133 """Format this edge for 'dot'."""
134 # If you change these styles, update also the legend in modulegraph.md
135 if self
._fromnode
.is_file_node() and self
._tonode
.is_file_node():
137 elif self
._edgetype
== EdgeType
.intramodule
:
139 elif self
._edgetype
== EdgeType
.test
:
140 # TODO: Consider if only some test edges should be made non-constraints
141 properties
= 'color=".33 .8 .8", style=dashed, constraint=no'
142 elif self
._edgetype
== EdgeType
.libimpl
:
143 properties
= 'color=".66 .8 .8", style=dashed'
144 elif self
._edgetype
== EdgeType
.pubimpl
:
145 properties
= 'color=black, style=dashed'
146 elif self
._edgetype
== EdgeType
.library
:
147 properties
= 'color=".66 .8 .8"'
148 elif self
._edgetype
== EdgeType
.public
:
149 properties
= 'color=black'
150 elif self
._edgetype
== EdgeType
.legacy
:
151 properties
= 'color=grey75'
152 elif self
._edgetype
== EdgeType
.cyclic
:
153 properties
= 'color=red, constraint=no'
155 properties
= 'color=red'
156 return '{0} -> {1} [{2}]'.format(self
._fromnode
.get_nodename(),
157 self
._tonode
.get_nodename(),
162 """Node in 'dot' graph."""
164 def __init__(self
, nodename
, label
, style
=None, properties
=None, is_file
=False):
165 """Create node with given attributes.
167 is_file does not affect the appearance of the node, but is used for
168 formatting edges between two files differently from other edges.
169 style and properties should be iterables with graphviz attributes for
172 Node can have child nodes. Such nodes are rendered as cluster
175 self
._nodename
= nodename
178 self
._style
= ','.join(style
)
182 self
._properties
= ', '.join(properties
)
184 self
._properties
= None
185 self
._is
_file
= is_file
188 def add_child(self
, child
):
189 """Add a child node."""
190 self
._children
.append(child
)
192 def clear_children(self
):
193 """Remove all children from the node."""
196 def is_file_node(self
):
197 """Return True if the node was created with is_file=True."""
200 def get_nodename(self
):
201 """Get internal name of the node in 'dot'."""
202 return self
._nodename
204 def get_children(self
, recursive
=False):
205 """Get list of child nodes."""
207 result
= list(self
._children
)
208 for child
in self
._children
:
209 result
.extend(child
.get_children(recursive
=True))
212 return self
._children
215 """Format this node for 'dot'."""
216 # TODO: Take indent as a parameter to make output marginally nicer.
219 result
+= ' subgraph cluster_{0} {{\n' \
220 .format(self
._nodename
)
221 result
+= ' label = "{0}"\n'.format(self
._label
)
222 for child
in self
._children
:
223 result
+= child
.format()
226 properties
= 'label="{0}"'.format(self
._label
)
228 properties
+= ', ' + self
._properties
230 properties
+= ', style="{0}"'.format(self
._style
)
231 result
+= ' {0} [{1}]\n'.format(self
._nodename
, properties
)
237 """Graph for 'dot'."""
239 def __init__(self
, nodes
, edges
):
240 """Create graph with given nodes and edges."""
241 self
._nodes
= set(nodes
)
243 self
._left
_to
_right
= False
244 self
._concentrate
= True
246 def set_options(self
, left_to_right
=None, concentrate
=None):
247 """Set output options for the graph."""
248 if left_to_right
!= None:
249 self
._left
_to
_right
= left_to_right
250 if concentrate
!= None:
251 self
._concentrate
= concentrate
253 def merge_nodes(self
, nodes
, target
):
254 """Merge a set of nodes into a single node.
256 All nodes from the list nodes are merged into the target node.
257 All edges to or from the merged nodes are rerouted to/from target
258 instead. Duplicate edges are not created. Instead, if an edge already
259 exists, the edge types are merged. All nodes from the list nodes are
260 removed from the graph after the merge is done.
267 for edge
in self
._edges
:
268 isfrom
= (edge
._fromnode
in nodes
)
269 isto
= (edge
._tonode
in nodes
)
273 if not edge
._tonode
in edgesfrom
:
274 edgesfrom
[edge
._tonode
] = \
275 Edge(target
, edge
._tonode
, edge
._edgetype
)
277 edgesfrom
[edge
._tonode
].merge_edge(edge
)
279 if not edge
._fromnode
in edgesto
:
280 edgesto
[edge
._fromnode
] = \
281 Edge(edge
._fromnode
, target
, edge
._edgetype
)
283 edgesto
[edge
._fromnode
].merge_edge(edge
)
285 newedges
.append(edge
)
286 newedges
.extend(edgesfrom
.values())
287 newedges
.extend(edgesto
.values())
288 self
._edges
= newedges
290 def collapse_node(self
, node
):
291 """Merge all children of a node into the node.
293 All child nodes are removed after the merge is done.
295 nodes
= node
.get_children(recursive
=True)
296 self
.merge_nodes(nodes
, node
)
297 node
.clear_children()
299 def write(self
, outfile
):
300 """Write the graph in 'dot' format."""
301 outfile
.write('digraph includedeps {\n')
302 if self
._left
_to
_right
:
303 outfile
.write(' rankdir = LR\n')
304 if self
._concentrate
:
305 outfile
.write(' concentrate = true\n')
306 outfile
.write(' node [fontname="FreeSans",fontsize=10,height=.2,'
308 for node
in self
._nodes
:
309 outfile
.write(node
.format())
310 for edge
in self
._edges
:
311 outfile
.write(' ' + edge
.format() + '\n')
314 class GraphBuilder(object):
316 """Builder for Graph objects from gmxtree.GromacsTree representation."""
318 def __init__(self
, tree
):
319 """Initialize builder for a given tree representation."""
322 def _create_file_node(self
, fileobj
, filenodes
):
323 """Create graph node for a file object.
325 filenodes is a dict() that maps file objects to their nodes, and is
326 updated by this call.
328 nodename
= re
.subn(r
'[-./]', '_', fileobj
.get_relpath())[0]
331 properties
.append('URL="\\ref {0}"'.format(fileobj
.get_name()))
332 if not fileobj
.get_module():
334 properties
.append('color=red')
335 if fileobj
.is_test_file():
336 style
.append('filled')
337 properties
.append('fillcolor=".33 .2 1"')
338 elif fileobj
.is_source_file():
339 style
.append('filled')
340 properties
.append('fillcolor=grey75')
341 elif fileobj
.get_api_type() == DocType
.public
:
342 style
.append('filled')
343 properties
.append('fillcolor=".66 .2 1"')
344 elif fileobj
.get_api_type() == DocType
.library
:
345 style
.append('filled')
346 properties
.append('fillcolor=".66 .5 1"')
347 node
= Node(nodename
, fileobj
.get_name(), style
, properties
, is_file
=True)
348 filenodes
[fileobj
] = node
351 def _get_file_edge_type(self
, fromfile
, tofile
):
352 """Get EdgeType for an edge between two file objects.
354 Determines the type for the edge from the information provided by
357 intramodule
= (fromfile
.get_module() == tofile
.get_module())
358 is_legacy
= not tofile
.api_type_is_reliable()
359 if fromfile
.get_module() == tofile
.get_module():
360 return EdgeType
.intramodule
361 elif tofile
.get_api_type() == DocType
.internal
and not tofile
.is_public():
363 return EdgeType
.legacy
365 return EdgeType
.undocumented
366 elif fromfile
.is_test_file():
368 elif tofile
.is_test_file():
369 return EdgeType
.undocumented
370 elif fromfile
.is_module_internal():
371 if tofile
.is_public():
372 return EdgeType
.pubimpl
373 elif tofile
.get_api_type() == DocType
.library
:
374 return EdgeType
.libimpl
376 return EdgeType
.legacy
377 elif not tofile
.is_documented():
378 return EdgeType
.undocumented
380 raise ValueError('Unknown edge type between {0} and {1}'
381 .format(fromfile
.get_relpath(), tofile
.get_relpath()))
382 elif fromfile
.get_api_type() == DocType
.library
:
383 return EdgeType
.library
384 elif fromfile
.is_public():
385 if tofile
.is_public():
386 return EdgeType
.public
388 return EdgeType
.undocumented
390 return EdgeType
.legacy
392 raise ValueError('Unknown edge type between {0} and {1}'
393 .format(fromfile
.get_relpath(), tofile
.get_relpath()))
395 def _create_file_edge(self
, fromfile
, tofile
, filenodes
):
396 """Create edge between two file objects.
398 Determines the type for the edge from the information provided by
401 edgetype
= self
._get
_file
_edge
_type
(fromfile
, tofile
)
402 return Edge(filenodes
[fromfile
], filenodes
[tofile
], edgetype
)
404 def _create_file_edges(self
, filenodes
):
405 """Create edges between all file nodes.
407 Create edges between file nodes specified in filenodes from all include
408 dependencies. An edge is created only if both ends of the dependency
409 are in the list of nodes.
412 for fileobj
in filenodes
.keys():
413 for includedfile
in fileobj
.get_includes():
414 otherfile
= includedfile
.get_file()
415 if otherfile
and otherfile
in filenodes
:
416 edge
= self
._create
_file
_edge
(fileobj
, otherfile
, filenodes
)
420 def _get_module_color(self
, modulegroup
):
421 # If you change these styles, update also the legend in modulegraph.md
422 if modulegroup
== 'legacy':
423 return 'fillcolor=grey75'
424 elif modulegroup
== 'analysismodules':
425 return 'fillcolor="0 .2 1"'
426 elif modulegroup
== 'utilitymodules':
427 return 'fillcolor=".08 .2 1"'
428 elif modulegroup
== 'mdrun':
429 return 'fillcolor=".75 .2 1"'
432 def _create_module_node(self
, module
):
433 """Create node for a module."""
436 properties
.append('shape=ellipse')
437 if module
.is_documented():
438 properties
.append('URL="\\ref {0}"'.format(module
.get_name()))
439 if not module
.is_documented():
440 fillcolor
= self
._get
_module
_color
('legacy')
442 fillcolor
= self
._get
_module
_color
(module
.get_group())
444 style
.append('filled')
445 properties
.append(fillcolor
)
446 rootdir
= module
.get_root_dir()
447 nodename
= 'module_' + re
.subn(r
'[-./]', '_', rootdir
.get_relpath())[0]
448 label
= module
.get_name()[7:]
449 node
= Node(nodename
, label
, style
, properties
)
452 def _create_module_edges(self
, modulenodes
):
453 """Create edges between all module nodes.
455 Create edges between module nodes specified in modulenodes from all
456 include dependencies. An edge is created only if both ends of the
457 dependency are in the list of nodes.
460 for moduleobj
in modulenodes
.keys():
461 for dep
in moduleobj
.get_dependencies():
462 othermodule
= dep
.get_other_module()
463 if othermodule
and othermodule
in modulenodes
:
464 if dep
.is_cycle_suppressed():
465 edgetype
= EdgeType
.cyclic
468 self
._get
_file
_edge
_type
(x
.get_including_file(), x
.get_file())
469 for x
in dep
.get_included_files()])
470 edge
= Edge(modulenodes
[moduleobj
], modulenodes
[othermodule
], edgetype
)
474 def create_modules_graph(self
):
475 """Create module dependency graph."""
478 libgromacsnode
= Node('libgromacs', 'libgromacs')
479 nodes
.append(libgromacsnode
)
480 for moduleobj
in self
._tree
.get_modules():
481 node
= self
._create
_module
_node
(moduleobj
)
482 if moduleobj
.get_root_dir().get_relpath().startswith('src/gromacs'):
483 libgromacsnode
.add_child(node
)
486 modulenodes
[moduleobj
] = node
487 edges
= self
._create
_module
_edges
(modulenodes
)
488 graph
= Graph(nodes
, edges
)
489 graph
.set_options(concentrate
=False)
492 def create_module_file_graph(self
, module
):
493 """Create file dependency graph for files within a module."""
496 for fileobj
in module
.get_files():
497 nodes
.append(self
._create
_file
_node
(fileobj
, filenodes
))
498 edges
= self
._create
_file
_edges
(filenodes
)
499 graph
= Graph(nodes
, edges
)
500 graph
.set_options(left_to_right
=True)
504 """Run the graph generation script."""
508 from optparse
import OptionParser
510 from gmxtree
import GromacsTree
511 from reporter
import Reporter
513 parser
= OptionParser()
514 parser
.add_option('-S', '--source-root',
515 help='Source tree root directory')
516 parser
.add_option('-B', '--build-root',
517 help='Build tree root directory')
518 parser
.add_option('--ignore-cycles',
519 help='Set file with module dependencies to ignore in cycles')
520 parser
.add_option('-o', '--outdir', default
='.',
521 help='Specify output directory for graphs')
522 parser
.add_option('-q', '--quiet', action
='store_true',
523 help='Do not write status messages')
524 options
, args
= parser
.parse_args()
526 reporter
= Reporter(quiet
=True)
528 if not options
.quiet
:
529 sys
.stderr
.write('Scanning source tree...\n')
530 tree
= GromacsTree(options
.source_root
, options
.build_root
, reporter
)
531 if not options
.quiet
:
532 sys
.stderr
.write('Reading source files...\n')
534 if options
.ignore_cycles
:
535 tree
.load_cycle_suppression_list(options
.ignore_cycles
)
536 if not options
.quiet
:
537 sys
.stderr
.write('Reading Doxygen XML files...\n')
538 tree
.load_xml(only_files
=True)
540 if not options
.quiet
:
541 sys
.stderr
.write('Writing graphs...\n')
542 graphbuilder
= GraphBuilder(tree
)
543 if not os
.path
.exists(options
.outdir
):
544 os
.mkdir(options
.outdir
)
546 filename
= os
.path
.join(options
.outdir
, 'module-deps.dot')
547 graph
= graphbuilder
.create_modules_graph()
548 with
open(filename
, 'w') as outfile
:
551 # Skip some modules that are too big to make any sense
552 skippedmodules
= ('gmxlib', 'mdlib', 'gmxana', 'gmxpreprocess')
553 for module
in tree
.get_modules():
554 if not module
.get_name()[7:] in skippedmodules
:
555 filename
= '{0}-deps.dot'.format(module
.get_name())
556 filename
= os
.path
.join(options
.outdir
, filename
)
557 graph
= graphbuilder
.create_module_file_graph(module
)
558 with
open(filename
, 'w') as outfile
:
561 if __name__
== '__main__':