+++ /dev/null
-#
-# This file is part of the GROMACS molecular simulation package.
-#
-# Copyright (c) 2012,2013,2014, by the GROMACS development team, led by
-# Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
-# and including many others, as listed in the AUTHORS file in the
-# top-level source directory and at http://www.gromacs.org.
-#
-# GROMACS is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public License
-# as published by the Free Software Foundation; either version 2.1
-# of the License, or (at your option) any later version.
-#
-# GROMACS is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with GROMACS; if not, see
-# http://www.gnu.org/licenses, or write to the Free Software Foundation,
-# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-#
-# If you want to redistribute modifications to GROMACS, please
-# consider that scientific software is very special. Version
-# control is crucial - bugs must be traceable. We will be happy to
-# consider code for inclusion in the official distribution, but
-# derived work must not be called official GROMACS. Details are found
-# in the README & COPYING files - if they are missing, get the
-# official version at http://www.gromacs.org.
-#
-# To help us fund GROMACS development, we humbly ask that you cite
-# the research papers on the package. Check out http://www.gromacs.org.
-
-function (generate_module_file_list SRCDIR OUTFILE MODE)
- set(_file_list)
- file(GLOB_RECURSE _file_list
- ${SRCDIR}/src/gromacs/*.cpp
- ${SRCDIR}/src/gromacs/*.c
- ${SRCDIR}/src/gromacs/*.cu
- ${SRCDIR}/src/gromacs/*.h
- ${SRCDIR}/src/gromacs/*.cuh
- ${SRCDIR}/src/testutils/*.cpp
- ${SRCDIR}/src/testutils/*.h
- )
- string(REPLACE ";" "\n" _file_list "${_file_list}")
- file(WRITE ${OUTFILE} "${_file_list}")
-endfunction ()
-
-function (generate_installed_file_list SRCDIR BUILDDIR OUTFILE)
- file(GLOB_RECURSE INSTALL_FILE_LIST "${BUILDDIR}/cmake_install.cmake")
- set(MATCH_REGEX "${SRCDIR}/.*\\.h")
- set(HEADER_LIST)
- foreach (INSTALL_FILE ${INSTALL_FILE_LIST})
- file(STRINGS ${INSTALL_FILE} HEADER_LINES REGEX "${MATCH_REGEX}")
- foreach (HEADER_LINE ${HEADER_LINES})
- string (REGEX MATCH "${MATCH_REGEX}" HEADER "${HEADER_LINE}")
- list(APPEND HEADER_LIST "${HEADER}")
- endforeach ()
- endforeach ()
- string(REPLACE ";" "\n" HEADER_LIST "${HEADER_LIST}")
- file(WRITE ${OUTFILE} "${HEADER_LIST}")
-endfunction ()
-
-if (NOT DEFINED SRCDIR OR NOT DEFINED BUILDDIR OR NOT DEFINED OUTDIR)
- message(FATAL_ERROR "Required input variable (SRCDIR, BUILDDIR, OUTDIR) not set")
-endif()
-
-if (NOT DEFINED PYTHON_EXECUTABLE)
- set(PYTHON_EXECUTABLE python)
-endif()
-
-if (NOT DEFINED MODE)
- set(MODE "CHECK")
-endif()
-
-if (MODE STREQUAL "CHECK")
- set(GRAPHOPTIONS --check)
-elseif (MODE STREQUAL "CHECKDOC")
- # TODO: Add --warn-undoc after most code has at least rudimentary comments.
- set(GRAPHOPTIONS --check --check-doc)
-elseif (MODE STREQUAL "GRAPHS")
- set(GRAPHOPTIONS
- --module-graph module-deps.dot --module-file-graphs
- -o ${OUTDIR})
-else()
- message(FATAL_ERROR "Unknown mode ${MODE}")
-endif()
-
-file(MAKE_DIRECTORY ${OUTDIR})
-generate_module_file_list(${SRCDIR} ${OUTDIR}/module-files.txt ${MODE})
-generate_installed_file_list(${SRCDIR} ${BUILDDIR} ${OUTDIR}/installed-headers.txt)
-execute_process(COMMAND ${PYTHON_EXECUTABLE} ${SRCDIR}/admin/includedeps.py
- -f ${OUTDIR}/module-files.txt
- --installed ${OUTDIR}/installed-headers.txt
- -R ${SRCDIR}/src -R ${BUILDDIR}/src
- -I ${SRCDIR}/src/gromacs/legacyheaders
- -I ${BUILDDIR}/src/gromacs/utility
- ${GRAPHOPTIONS})
-
-if (MODE STREQUAL "GRAPHS" AND DOT_EXECUTABLE)
- file(GLOB DOT_INPUT_FILES ${OUTDIR}/*.dot)
- execute_process(COMMAND ${DOT_EXECUTABLE} -Tpng -O ${DOT_INPUT_FILES})
-endif()
+++ /dev/null
-#!/usr/bin/python
-#
-# This file is part of the GROMACS molecular simulation package.
-#
-# Copyright (c) 2012,2013,2014, by the GROMACS development team, led by
-# Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
-# and including many others, as listed in the AUTHORS file in the
-# top-level source directory and at http://www.gromacs.org.
-#
-# GROMACS is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public License
-# as published by the Free Software Foundation; either version 2.1
-# of the License, or (at your option) any later version.
-#
-# GROMACS is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with GROMACS; if not, see
-# http://www.gnu.org/licenses, or write to the Free Software Foundation,
-# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-#
-# If you want to redistribute modifications to GROMACS, please
-# consider that scientific software is very special. Version
-# control is crucial - bugs must be traceable. We will be happy to
-# consider code for inclusion in the official distribution, but
-# derived work must not be called official GROMACS. Details are found
-# in the README & COPYING files - if they are missing, get the
-# official version at http://www.gromacs.org.
-#
-# To help us fund GROMACS development, we humbly ask that you cite
-# the research papers on the package. Check out http://www.gromacs.org.
-
-"""Check and generate include dependency graphs for Gromacs.
-
-This script can do a few things related to include file dependencies:
- - Check that there are no broken dependencies between installed headers.
- - Check that documentated usage of a header matches its installation status
- and usage from other modules.
- - Generate two types of include dependency graphs: per-file or per-module
- (where module is equivalent to a subdirectory).
-It is intended to be run on a subset of files under the src/ directory.
-Output format for the graphs is suitable for processing with 'dot'.
-
-FILE GRAPHS
-
-The legend for per-file graph nodes:
- gray: source files
- light blue: public headers
- dark blue: library headers
- no background: other files
-
-MODULE GRAPHS
-
-Module graph will contain one node for each top-level subdirectory under src/,
-except that the src/gromacs/ directory will be expanded one level further.
-Legacy modules have gray background.
-
-The legend for per-module graph links (a link with a certain color indicates
-that types above it in the list are not present):
- red: invalid dependency
- grey: legacy dependency (dependency on undocumented file, or to
- legacy directories)
- solid black: public header depends on the other module
- solid blue: library header depends on the other module
- dashed blue: source file depends on a library header in the other module
- dashed black: source file depends on a public header in the other module
- dashed green: test file depends on the other module
-"""
-
-import os.path
-import re
-import sys
-
-from optparse import OptionParser
-
-class ErrorReporter(object):
- def input_warning(self, file_path, msg):
- sys.stderr.write('warning: {0}: {1}\n'.format(file_path, msg))
-
- def error(self, file_path, msg):
- sys.stderr.write('error: {0}: {1}\n'.format(file_path, msg))
-
-class Link(object):
-
- """Link between two node objects.
-
- Signifies an include dependency between the two nodes, and manages types
- associated with the dependencies.
- """
-
- _priorities = {
- 'undocumented': 1,
- 'legacy': 2,
- 'intramodule': 3,
- 'public': 4,
- 'library': 5,
- 'libimpl': 6,
- 'pubimpl': 7,
- 'test': 8}
-
- def __init__(self, fromnode, tonode, link_type):
- self.fromnode = fromnode
- self.tonode = tonode
- self.link_type = link_type
- if link_type not in Link._priorities:
- raise ValueError('Unknown link type {0}'.format(link_type))
-
- def merge_link(self, other):
- """Merge another link into this one and choose an appropriate type.
-
- Updates the type of this link based on the types of the merged links.
- """
- if Link._priorities[other.link_type] < Link._priorities[self.link_type]:
- self.link_type = other.link_type
-
- def format(self):
- """Format this link for 'dot'."""
- if self.fromnode.is_file_node() and self.tonode.is_file_node():
- properties = ''
- elif self.link_type == 'intramodule':
- properties = ''
- elif self.link_type == 'test':
- properties = 'color=".33 .8 .8", style=dashed'
- elif self.link_type == 'libimpl':
- properties = 'color=".66 .8 .8", style=dashed'
- elif self.link_type == 'pubimpl':
- properties = 'color=black, style=dashed'
- elif self.link_type == 'library':
- properties = 'color=".66 .8 .8"'
- elif self.link_type == 'public':
- properties = 'color=black'
- elif self.link_type == 'legacy':
- properties = 'color=grey75'
- else: # undocumented
- properties = 'color=red'
- return '{0} -> {1} [{2}]'.format(self.fromnode.nodename,
- self.tonode.nodename,
- properties)
-
-class Node(object):
- def __init__(self, nodename, label, properties, is_file):
- self.nodename = nodename
- self.label = label
- self._properties = properties
- self._is_file = is_file
- self.children = []
- self.root = False
-
- def set_root(self):
- self.root = True
-
- def add_child(self, child):
- self.children.append(child)
-
- def remove_child(self, child):
- self.children.remove(child)
-
- def clear_children(self):
- self.children = []
-
- def is_file_node(self):
- return self._is_file
-
- def get_children(self, recursive=False):
- if recursive:
- result = list(self.children)
- for child in self.children:
- result.extend(child.get_children(recursive=True))
- return result
- else:
- return self.children
-
- def format(self):
- """Format this node for 'dot'."""
- result = ''
- if self.children:
- if not self.root:
- result += ' subgraph cluster_{0} {{\n' \
- .format(self.nodename)
- result += ' label = "{0}"\n'.format(self.label)
- for child in self.children:
- result += child.format()
- if not self.root:
- result += ' }\n'
- else:
- properties = 'label="{0}"'.format(self.label)
- if self._properties:
- properties += ', ' + self._properties
- result += ' {0} [{1}]\n'.format(self.nodename, properties)
- return result
-
-
-class Graph(object):
- def __init__(self, nodes, links):
- self.nodes = set(nodes)
- self.links = links
- self.left_to_right = False
- self.concentrate = True
-
- def set_options(self, left_to_right=None, concentrate=None):
- if left_to_right != None:
- self.left_to_right = left_to_right
- if concentrate != None:
- self.concentrate = concentrate
-
- def prune_links(self):
- nodes = set()
- for node in self.nodes:
- nodes.update(node.get_children(recursive=True))
- newlinks = []
- for link in self.links:
- if link.fromnode in nodes and link.tonode in nodes:
- newlinks.append(link)
- self.links = newlinks
-
- def merge_nodes(self, nodes, target):
- nodes = set(nodes)
- nodes.add(target)
- newlinks = []
- linksto = dict()
- linksfrom = dict()
- for link in self.links:
- isfrom = (link.fromnode in nodes)
- isto = (link.tonode in nodes)
- if isfrom and isto:
- pass
- elif isfrom:
- if not link.tonode in linksfrom:
- linksfrom[link.tonode] = \
- Link(target, link.tonode, link.link_type)
- else:
- linksfrom[link.tonode].merge_link(link)
- elif isto:
- if not link.fromnode in linksto:
- linksto[link.fromnode] = \
- Link(link.fromnode, target, link.link_type)
- else:
- linksto[link.fromnode].merge_link(link)
- else:
- newlinks.append(link)
- newlinks.extend(linksfrom.values())
- newlinks.extend(linksto.values())
- self.links = newlinks
-
- def collapse_node(self, node):
- nodes = node.get_children(recursive=True)
- self.merge_nodes(nodes, node)
- node.clear_children()
-
- def write(self, outfile):
- outfile.write('digraph includedeps {\n')
- if self.left_to_right:
- outfile.write(' rankdir = LR\n')
- if self.concentrate:
- outfile.write(' concentrate = true\n')
- outfile.write(' node [fontname="FreeSans",fontsize=10,height=.2,'
- 'shape=box]\n')
- for link in self.links:
- outfile.write(' ' + link.format() + '\n')
- for node in self.nodes:
- outfile.write(node.format())
- outfile.write('}\n')
-
-
-def find_include_file(filename, includedirs):
- """Find full path to filename, looking in a set of directories."""
- for includedir in includedirs:
- fullpath = os.path.abspath(os.path.join(includedir, filename))
- if os.path.exists(fullpath):
- return fullpath
- return None
-
-
-class IncludedFile(object):
- def __init__(self, included_file, included_path, is_relative, is_system):
- self._included_file = included_file
- self._included_path = included_path
- #self._used_include_path = used_include_path
- self._is_relative = is_relative
- self._is_system = is_system
-
-
-class File(object):
- def __init__(self, path, module):
- self.path = path
- self.name = os.path.basename(path)
- self.module = module
- if module.name == 'tests' or module.name == 'legacytests':
- self.type = 'test'
- elif re.search(r'\.c(pp|u)?$', self.name) != None:
- self.type = 'source'
- else:
- self.type = 'header'
- self.doctype = 'none'
- #headername = re.sub(r'\.cpp$', '.h', self.name)
- #implheadername = re.sub(r'\.cpp$', '-impl.h', self.name)
- self._included = []
- self.installed = False
-
- def is_documented(self):
- return self.doctype != 'none'
-
- def is_installed(self):
- return self.installed
-
- def set_installed(self, reporter):
- if self.type != 'header':
- reporter.input_warning(self.path,
- 'installing {0} file'.format(self.type))
- return
- self.installed = True
-
- def get_included_files(self):
- return self._included
-
- def scan_include_file(self, line, allfiles, selfdir, includedirs,
- ignorelist, reporter):
- """Process #include directive during scan().
-
- Searches for the included file in given directories, does some checks,
- and adds the dependency link to the other file if applicable.
- """
- fullpath = None
- includedpath = None
- includedfile = None
- is_system = False
- is_relative = False
- match = re.match(r'#include *<([^>]*)>', line)
- if match:
- includedpath = match.group(1)
- is_system = True
- fullpath = find_include_file(includedpath, includedirs)
- else:
- match = re.match(r'#include *"([^"]*)"', line)
- if match:
- includedpath = match.group(1)
- fullpath = os.path.abspath(os.path.join(selfdir, includedpath))
- #if os.path.abspath(fullpath) in ignorelist:
- # return
- if os.path.exists(fullpath):
- is_relative = True
- else:
- fullpath = find_include_file(includedpath, includedirs)
- if not fullpath:
- if not includedpath in ('corewrap.h', 'tmpi_config.h'):
- reporter.input_warning(self.path,
- 'included file "{0}" not found'
- .format(includedpath))
- if not includedpath:
- reporter.input_warning(self.path, 'line "{0}" could not be parsed'
- .format(line))
- else:
- if fullpath and fullpath in allfiles:
- includedfile = allfiles[fullpath]
- #elif not dep in ignorelist:
- # depfile = File(dep, None)
- # files[dep] = depfile
- # file.add_dependency(depfile)
- # extrafiles.append(dep)
- self._included.append(IncludedFile(includedfile, includedpath,
- is_relative, is_system))
-
- def scan(self, filename, allfiles, includedirs, ignorelist, reporter):
- selfdir = os.path.dirname(filename)
- infileblock = False
- foundfileblock = False
- self.docmodule = None
- with open(filename, 'r') as scanfile:
- for line in scanfile:
- if line.startswith('#include'):
- self.scan_include_file(line, allfiles, selfdir,
- includedirs, ignorelist, reporter)
- continue
- if not foundfileblock:
- if infileblock:
- if r'*/' in line:
- infileblock = False
- foundfileblock = True
- continue
- if self.type == 'implheader':
- if line.startswith(r' * \inpublicapi'):
- self.type = 'publicheader'
- elif line.startswith(r' * \inlibraryapi'):
- self.type = 'libheader'
- match = re.match(r' \* \\ingroup module_([a-z_]*)', line)
- if match:
- if self.docmodule:
- reporter.error(self.path,
- 'file documented in multiple modules')
- self.docmodule = match.group(1)
- else:
- match = re.match(r'/\*! *(\\[a-z]*internal)? *\\file', line)
- if match:
- docspec = match.group(1)
- if not docspec:
- self.doctype = 'public'
- elif docspec == r'\libinternal':
- self.doctype = 'library'
- elif docspec == r'\internal':
- self.doctype = 'implementation'
- else:
- reporter.input_warning(self.path,
- 'unknown specifier "{0}"'.format(docspec))
- self.doctype = 'unknown'
- infileblock = True
- if self.type == 'header':
- # Default type if no other found
- self.type = 'implheader'
-
-
-class Module(object):
- def __init__(self, name, parent = None):
- self.parent = parent
- self.name = name
- if parent:
- self.fullname = parent.fullname + '_' + name
- else:
- self.fullname = 'module'
- self.files = []
- self.children = dict()
- self.is_top_level = (not parent or parent.name in ('', 'gromacs'))
-
- def get_parent(self):
- return self.parent
-
- def is_child(self, module):
- parent = module.parent
- while parent:
- if parent == self:
- return True
- parent = parent.parent
- return False
-
- def get_top_level_module(self):
- if self.is_top_level or not self.parent:
- return self
- return self.parent.get_top_level_module()
-
- def add_nested_file(self, modules, path):
- if len(modules) == 1:
- newfile = File(path, self)
- self.files.append(newfile)
- else:
- if not modules[0] in self.children:
- module = Module(modules[0], self)
- self.children[modules[0]] = module
- else:
- module = self.children[modules[0]]
- newfile = module.add_nested_file(modules[1:], path)
- return newfile
-
-
-class Dependencies(object):
- def __init__(self, rootdir, includedirs, installedfiles):
- self.files = dict()
- self.root = Module("")
- self.rootpath = []
- for root in rootdir:
- self.rootpath.append(os.path.abspath(root))
- if includedirs:
- self.includedirs = self.rootpath + includedirs
- else:
- self.includedirs = self.rootpath
- self.installedfiles = installedfiles
-
- def add_file(self, filename, reporter):
- fullpath = os.path.abspath(filename)
- for root in self.rootpath:
- if fullpath.startswith(root):
- relpath = fullpath[len(root)+1:]
- break
- else:
- reporter.input_warning(filename,
- 'input file not under root path, skipped')
- return
- modules = relpath.split(os.sep)
- newfile = self.root.add_nested_file(modules, relpath)
- if fullpath in self.installedfiles:
- newfile.set_installed(reporter)
- self.files[os.path.abspath(filename)] = newfile
-
- def scan_files(self, ignorelist, reporter):
- for (filename, scanfile) in self.files.iteritems():
- scanfile.scan(filename, self.files, self.includedirs, ignorelist,
- reporter)
-
- def get_toplevel_modules(self):
- result = []
- for module in self.root.children.itervalues():
- if module.name == 'gromacs':
- result.extend(module.children.itervalues())
- else:
- result.append(module)
- return result
-
-
-def _is_legacy_module(module):
- if module.name in ('legacyheaders', 'gmxlib', 'mdlib', 'gmxana', 'gmxpreprocess'):
- return True
- if module.get_parent():
- return _is_legacy_module(module.get_parent())
- return False
-
-
-class GraphBuilder(object):
- def __init__(self, deps):
- self._deps = deps
-
- def _create_file_node(self, fileobj, filenodes):
- nodename = re.subn(r'[-./]', '_', fileobj.path)[0]
- properties = []
- style = []
- properties.append('URL="\\ref {0}"'.format(fileobj.name))
- if not fileobj.module:
- style.append('bold')
- properties.append('color=red')
- if fileobj.type == 'source':
- style.append('filled')
- properties.append('fillcolor=grey75')
- elif fileobj.type == 'publicheader':
- style.append('filled')
- properties.append('fillcolor=".66 .2 1"')
- elif fileobj.type == 'libheader':
- style.append('filled')
- properties.append('fillcolor=".66 .5 1"')
- if style:
- properties.append('style="{0}"'.format(','.join(style)))
- node = Node(nodename, fileobj.name, ', '.join(properties), is_file=True)
- filenodes[fileobj] = node
- return node
-
- def _create_file_edge(self, fromfile, tofile, filenodes):
- intramodule = \
- (fromfile.module.get_top_level_module() == \
- tofile.module.get_top_level_module())
- is_legacy = _is_legacy_module(tofile.module)
- if tofile.type not in ('publicheader', 'libheader', 'header', 'test'):
- if intramodule:
- link_type = 'intramodule'
- elif is_legacy:
- link_type = 'legacy'
- else:
- link_type = 'undocumented'
- elif fromfile.type == 'test':
- link_type = 'test'
- elif tofile.type == 'test':
- link_type = 'undocumented'
- elif fromfile.type in ('source', 'header', 'implheader') and \
- not fromfile.is_installed():
- if intramodule:
- link_type = 'intramodule'
- elif tofile.type == 'publicheader':
- link_type = 'pubimpl'
- elif tofile.type == 'libheader':
- link_type = 'libimpl'
- elif is_legacy:
- link_type = 'legacy'
- elif not tofile.is_documented():
- link_type = 'legacy'
- else:
- raise ValueError('Unknown link type between {0} and {1}'
- .format(fromfile.path, tofile.path))
- elif fromfile.type == 'libheader':
- link_type = 'library'
- elif fromfile.type == 'publicheader' or fromfile.is_installed():
- if tofile.type == 'publicheader' or tofile.doctype == 'public' or \
- (tofile.is_installed() and not tofile.is_documented()):
- link_type = 'public'
- else:
- link_type = 'undocumented'
- else:
- raise ValueError('Unknown link type between {0} and {1}'
- .format(fromfile.path, tofile.path))
- return Link(filenodes[fromfile], filenodes[tofile], link_type)
-
- def _create_file_edges(self, fileobj, filenodes):
- links = []
- if fileobj in filenodes:
- for includedfile in fileobj.get_included_files():
- otherfile = includedfile._included_file
- if otherfile and otherfile in filenodes:
- link = self._create_file_edge(fileobj, otherfile, filenodes)
- links.append(link)
- return links
-
- def create_module_node(self, module, filenodes):
- properties = 'shape=ellipse, URL="\\ref module_{0}"'.format(module.name)
- if _is_legacy_module(module):
- properties += 'style=filled, fillcolor=grey75'
- node = Node(module.fullname, module.name, properties, is_file=False)
- for childfile in module.files:
- node.add_child(self._create_file_node(childfile, filenodes))
- for childmodule in module.children.itervalues():
- node.add_child(self.create_module_node(childmodule, filenodes))
- return node
-
- def create_file_graph(self):
- filenodes = dict()
- rootnode = self.create_module_node(self._deps.root, filenodes)
- rootnode.set_root()
- links = []
- for scanfile in self._deps.files.itervalues():
- links.extend(self._create_file_edges(scanfile, filenodes))
- graph = Graph([rootnode], links)
- return graph
-
- def create_modules_graph(self):
- filenodes = dict()
- rootnode = self.create_module_node(self._deps.root, filenodes)
- rootnode.set_root()
- links = []
- for scanfile in self._deps.files.itervalues():
- links.extend(self._create_file_edges(scanfile, filenodes))
- graph = Graph([rootnode], links)
- for node in rootnode.get_children():
- if node.label == 'gromacs':
- module_nodes = []
- header_nodes = []
- for child in node.get_children():
- if child.is_file_node():
- header_nodes.append(child)
- else:
- graph.collapse_node(child)
- module_nodes.append(child)
- for header in header_nodes:
- for module in module_nodes:
- if header.nodename.startswith(module.nodename[7:]):
- # graph.merge_nodes([header], module)
- node.remove_child(header)
- break
- else:
- graph.collapse_node(node)
- graph.set_options(concentrate=False)
- graph.prune_links()
- return graph
-
- def create_module_file_graph(self, module):
- filenodes = dict()
- rootnode = self.create_module_node(module, filenodes)
- rootnode.set_root()
- links = []
- for scanfile in self._deps.files.itervalues():
- links.extend(self._create_file_edges(scanfile, filenodes))
- graph = Graph([rootnode], links)
- graph.prune_links()
- return graph
-
-
-def print_module_graph(outfile, graphbuilder, options):
- graph = graphbuilder.create_modules_graph()
- graph.write(outfile)
-
-def print_file_graph(outfile, graphbuilder, options):
- graph = graphbuilder.create_file_graph()
- graph.set_options(left_to_right=options.left_to_right)
- graph.write(outfile)
- #if options.source_at_top:
- # sourcenodes = []
- # for file in deps.files.itervalues():
- # if file.sourcefile:
- # sourcenodes.append(file.nodename)
- # if sourcenodes:
- # outfile.write(' { rank = min; ' + '; '.join(sourcenodes) + '}\n')
- #if options.with_external and options.external_at_bottom:
- # extnodes = []
- # for file in deps.files.itervalues():
- # if not file.module:
- # extnodes.append(file.nodename)
- # if extnodes:
- # outfile.write(' { rank = max; ' + '; '.join(extnodes) + '}\n')
-
-def print_module_file_graph(outfile, graphbuilder, module, options):
- graph = graphbuilder.create_module_file_graph(module)
- graph.set_options(left_to_right=options.left_to_right)
- graph.write(outfile)
-
-def main():
- parser = OptionParser()
- parser.add_option('-f', '--files',
- help='Read list of input files from given file')
- parser.add_option('--installed',
- help='Read list of installed files from given file')
- parser.add_option('-R', '--rootdir', action='append',
- help='Remove this prefix from all files')
- parser.add_option('-I', '--includedir', action='append',
- help='Specify additional directories to search for '
- 'include files')
- parser.add_option('-o', '--outdir', default='.',
- help='Specify output directory for graphs')
- #parser.add_option('--source-at-top', action='store_true',
- # help='Force source files at the top of the graph')
- #parser.add_option('--with-external', action='store_true',
- # help='Include external dependencies in the graph')
- #parser.add_option('--external-at-bottom', action='store_true',
- # help='Force external dependencies files at the bottom '
- # 'of the graph')
- parser.add_option('--left-to-right', action='store_true',
- help='Lay out from left to right')
- parser.add_option('--file-graph',
- help='Write graph for individual files')
- parser.add_option('--module-graph',
- help='Write graph for modules')
- parser.add_option('--module-file-graphs', action='store_true',
- help='Write file graphs for each module')
- options, args = parser.parse_args()
-
- # Constructs lists of files
- filelist = []
- ignorelist = []
- installedlist = []
- if options.files:
- with open(options.files, 'r') as outfile:
- for line in outfile:
- if line.startswith('!'):
- ignorelist.append(os.path.abspath(line[1:].strip()))
- else:
- filelist.append(line.strip())
- filelist.extend(args)
- if options.installed:
- with open(options.installed, 'r') as outfile:
- for line in outfile:
- installedlist.append(line.strip())
-
- # Creates objects for all files and modules
- reporter = ErrorReporter()
- deps = Dependencies(options.rootdir, options.includedir, installedlist)
- for filename in filelist:
- deps.add_file(filename, reporter)
-
- deps.scan_files(ignorelist, reporter)
-
- #if options.with_external:
- # for filename in extrafiles:
- # file = files[filename]
- # if os.path.exists(filename):
- # with open(filename, 'r') as outfile:
- # for line in outfile:
- # if not file.api:
- # if line.startswith(' * \inpublicapi'):
- # file.api = "public"
- # elif line.startswith(' * \inlibraryapi'):
- # file.api = "library"
-
- # Prints out the graph
- graphbuilder = GraphBuilder(deps)
- if options.module_graph:
- graphpath = os.path.join(options.outdir, options.module_graph)
- with open(graphpath, 'w') as outfile:
- print_module_graph(outfile, graphbuilder, options)
- if options.file_graph:
- graphpath = os.path.join(options.outdir, options.file_graph)
- with open(graphpath, 'w') as outfile:
- print_file_graph(outfile, graphbuilder, options)
- if options.module_file_graphs:
- options.left_to_right = True
- for module in deps.get_toplevel_modules():
- if not _is_legacy_module(module):
- filename = 'module_{0}-deps.dot'.format(module.name)
- filename = os.path.join(options.outdir, filename)
- with open(filename, 'w') as outfile:
- print_module_file_graph(outfile, graphbuilder, module, options)
-
-main()
add_custom_target(doc-check COMMAND ${doc_check_command}
COMMENT "Checking Doxygen documentation" VERBATIM)
add_dependencies(doc-check doc-xml find-installed-headers)
- # This target is the same as doc-check, but it doesn't rerun the
+
+ set(graphdir ${CMAKE_CURRENT_BINARY_DIR}/depgraphs)
+ set(dep_graphs_command_python
+ ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/graphbuilder.py
+ -S ${CMAKE_SOURCE_DIR} -B ${CMAKE_BINARY_DIR}
+ --installed ${CMAKE_CURRENT_BINARY_DIR}/installed-headers.txt
+ -o ${CMAKE_CURRENT_BINARY_DIR}/depgraphs)
+ set(dep_graphs_command_dot
+ ${CMAKE_COMMAND} -DGRAPHDIR=${graphdir}
+ -DDOT_EXECUTABLE=${DOXYGEN_DOT_EXECUTABLE}
+ -P ${CMAKE_CURRENT_SOURCE_DIR}/generateGraphs.cmake)
+ add_custom_target(dep-graphs
+ COMMAND ${dep_graphs_command_python}
+ COMMAND ${dep_graphs_command_dot}
+ COMMENT "Generating include dependency graphs" VERBATIM)
+ add_dependencies(dep-graphs doc-xml find-installed-headers)
+
+ # These targets are the same as above, but they don't rerun the
# dependencies each time, making it faster and more convenient for
# testing.
add_custom_target(doc-check-fast COMMAND ${doc_check_command}
COMMENT "Checking Doxygen documentation" VERBATIM)
+ add_custom_target(dep-graphs-fast
+ COMMAND ${dep_graphs_command_python}
+ COMMAND ${dep_graphs_command_dot}
+ COMMENT "Generating include dependency graphs" VERBATIM)
endif()
endif()
-
-if (USE_PYTHON_SCRIPTS)
- add_custom_target(depgraphs
- ${CMAKE_COMMAND}
- -DSRCDIR=${CMAKE_SOURCE_DIR}
- -DBUILDDIR=${CMAKE_BINARY_DIR}
- -DOUTDIR=${CMAKE_BINARY_DIR}/doxygen/depgraphs
- -DPYTHON_EXECUTABLE=${PYTHON_EXECUTABLE}
- -DDOT_EXECUTABLE=${DOXYGEN_DOT_EXECUTABLE}
- -DMODE=GRAPHS
- -P ${CMAKE_SOURCE_DIR}/admin/includedeps.cmake
- COMMENT "Generating include dependency graphs" VERBATIM)
-endif()
parsing are stored in `doxygen/doxygen-xml.log` in the build tree, similar to
other Doxygen runs.
-There is an additional target `depgraphs` that generates include dependency
-graphs. This is in the process of getting rewritten to take advantage of the
-mechanisms used for `doc-check`. One graph is
-produced that shows all the modules under `src/gromacs/`, and their include
-dependencies. Additionally, a file-level graph is produced for each module,
-showing the include dependencies within that module. Currently, these are
-mostly for eye candy, but they can also be used for analyzing problematic
-dependencies to clean up the architecture. A legend for the graphs is
-currently included as comments in `admin/includedeps.py`. The output is put in
-`doxygen/depgraphs/`.
-
-These targets require Python 2.7 (other versions may work, but have not been
-tested); the graph generation additionally requires `graphviz`.
+The `doc-check` target requires Python 2.7 (other versions may work, but have
+not been tested).
+Include dependency graphs
+-------------------------
+
+The build system also provides an `dep-graphs` target that generates include
+dependency graphs with some additional annotations.
+One graph is produced that shows all the modules under `src/gromacs/`, and
+their include dependencies. Additionally, a file-level graph is produced for
+each module, showing the include dependencies within that module. Currently,
+these are mostly for eye candy, but they can also be used for analyzing
+problematic dependencies to clean up the architecture.
+The output is put in `doxygen/depgraphs/` in the build tree.
+
+As with `doc-check`, Python 2.7 is required (other versions may work, but have
+not been tested). To get `.png` versions of the graphs, `graphviz` is
+additionally required.
+
+### Module graph ###
+
+The graph is written into `module-deps.dot.png`.
+
+Node colors:
+<dl>
+<dt>gray background</dt>
+<dd>undocumented module</dd>
+<dt>orange background</dt>
+<dd>documented utility modules</dd>
+<dt>red background</dt>
+<dd>documented analysis modules</dd>
+<dt>violet background</dt>
+<dd>documented MD execution modules</dd>
+<dt>light blue border</dt>
+<dd>module contains public API (installed) headers</dd>
+</dl>
+
+Edge colors (an edge with a certain color indicates that types above it in the
+list are not present):
+<dl>
+<dt>red</dt>
+<dd>invalid dependency</dd>
+<dt>gray</dt>
+<dd>legacy dependency
+(dependency on undocumented file, or to undocumented directories)</dd>
+<dt>solid black</dt>
+<dd>public header depends on the other module</dd>
+<dt>solid blue</dt>
+<dd>library header depends on the other module</dd>
+<dt>dashed blue</dt>
+<dd>source file depends on library header in the other module</dd>
+<dt>dashed black</dt>
+<dd>source file depends on public header in the other module</dd>
+<dt>dashed green</dt>
+<dd>test file depends on the other module</dd>
+</dl>
+
+### File graph ###
+
+The graphs are written to <em>module_name</em>`-deps.dot.png`.
+
+Node colors:
+<dl>
+<dt>light blue</dt>
+<dd>public API (installed) headers</dd>
+<dt>dark blue</dt>
+<dd>library API headers</dd>
+<dt>gray</dt>
+<dd>source files</dd>
+<dt>light green</dt>
+<dd>test files</dd>
+<dt>white</dt>
+<dd>other files</dd>
+</dl>
+
+Each edge signifies an include dependency; there is no additional information
+currently included.
Documenting specific code constructs
====================================
This constructs relations between compound entities, and initializes other
attributes for the entities.
+ load_file_details() does the same as load_details(), except that it leaves
+ those compound XML files unloaded that do not affect file objects or their
+ parent hierarchy. This saves some time if details for actual code
+ constructs like namespaces, classes or members are not necessary.
+
merge_duplicates() can then be called to remove members with different IDs,
but that actually reference the same code entity. For some reason, Doxygen
seems to produce these in certain cases.
member.add_parent_compound(compound)
compound.add_member(member)
+ def load_file_details(self):
+ """Load detailed XML files for all files and possible parents of files."""
+ for compound in self._compounds.itervalues():
+ if isinstance(compound, (File, Directory, Group)):
+ compound.load_details()
+ if isinstance(compound, File):
+ self._files[compound.get_path()] = compound
+
def load_details(self):
"""Load detailed XML files for each compound."""
for compound in self._compounds.itervalues():
--- /dev/null
+#
+# This file is part of the GROMACS molecular simulation package.
+#
+# Copyright (c) 2014, by the GROMACS development team, led by
+# Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
+# and including many others, as listed in the AUTHORS file in the
+# top-level source directory and at http://www.gromacs.org.
+#
+# GROMACS is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License
+# as published by the Free Software Foundation; either version 2.1
+# of the License, or (at your option) any later version.
+#
+# GROMACS is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with GROMACS; if not, see
+# http://www.gnu.org/licenses, or write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# If you want to redistribute modifications to GROMACS, please
+# consider that scientific software is very special. Version
+# control is crucial - bugs must be traceable. We will be happy to
+# consider code for inclusion in the official distribution, but
+# derived work must not be called official GROMACS. Details are found
+# in the README & COPYING files - if they are missing, get the
+# official version at http://www.gromacs.org.
+#
+# To help us fund GROMACS development, we humbly ask that you cite
+# the research papers on the package. Check out http://www.gromacs.org.
+
+if (DOT_EXECUTABLE)
+ message("Running dot...")
+ file(GLOB DOT_INPUT_FILES ${GRAPHDIR}/*.dot)
+ execute_process(COMMAND ${DOT_EXECUTABLE} -Tpng -O ${DOT_INPUT_FILES})
+endif()
"""Information about an #include directive in a file."""
- def __init__(self, path, lineno, included_file, included_path, is_relative, is_system):
- self._path = path
+ def __init__(self, abspath, lineno, included_file, included_path, is_relative, is_system):
+ self._abspath = abspath
self._line_number = lineno
self._included_file = included_file
self._included_path = included_path
return self._included_file
def get_reporter_location(self):
- return reporter.Location(self._path, self._line_number)
+ return reporter.Location(self._abspath, self._line_number)
class File(object):
"""Source/header file in the GROMACS tree."""
- def __init__(self, path, directory):
+ def __init__(self, abspath, relpath, directory):
"""Initialize a file representation with basic information."""
- self._path = path
+ self._abspath = abspath
+ self._relpath = relpath
self._dir = directory
self._rawdoc = None
self._installed = False
- extension = os.path.splitext(path)[1]
+ extension = os.path.splitext(abspath)[1]
self._sourcefile = (extension in ('.c', '.cc', '.cpp', '.cu'))
self._apitype = DocType.none
self._modules = set()
self._includes = []
+ directory.add_file(self)
def set_doc_xml(self, rawdoc, sourcetree):
"""Assiociate Doxygen documentation entity with the file."""
if is_system:
fileobj = sourcetree.find_include_file(includedpath)
else:
- fullpath = os.path.join(self._dir.get_path(), includedpath)
+ fullpath = os.path.join(self._dir.get_abspath(), includedpath)
fullpath = os.path.abspath(fullpath)
if os.path.exists(fullpath):
is_relative = True
fileobj = sourcetree.get_file(fullpath)
else:
fileobj = sourcetree.find_include_file(includedpath)
- self._includes.append(IncludedFile(self.get_path(), lineno, fileobj, includedpath,
+ self._includes.append(IncludedFile(self.get_abspath(), lineno, fileobj, includedpath,
is_relative, is_system))
def scan_contents(self, sourcetree):
"""Scan the file contents and initialize information based on it."""
# TODO: Consider a more robust regex.
include_re = r'^#\s*include\s+(?P<quote>["<])(?P<path>[^">]*)[">]'
- with open(self._path, 'r') as scanfile:
+ with open(self._abspath, 'r') as scanfile:
for lineno, line in enumerate(scanfile, 1):
match = re.match(include_re, line)
if match:
sourcetree)
def get_reporter_location(self):
- return reporter.Location(self._path, None)
+ return reporter.Location(self._abspath, None)
def is_installed(self):
return self._installed
def has_brief_description(self):
return self._rawdoc and self._rawdoc.has_brief_description()
- def get_path(self):
- return self._path
+ def get_abspath(self):
+ return self._abspath
+
+ def get_relpath(self):
+ return self._relpath
+
+ def get_name(self):
+ return os.path.basename(self._abspath)
def get_documentation_type(self):
if not self._rawdoc:
"""(Sub)directory in the GROMACS tree."""
- def __init__(self, path, parent):
+ def __init__(self, abspath, relpath, parent):
"""Initialize a file representation with basic information."""
- self._path = path
- self._name = os.path.basename(path)
+ self._abspath = abspath
+ self._relpath = relpath
+ self._name = os.path.basename(abspath)
self._parent = parent
self._rawdoc = None
self._module = None
self._is_test_dir = False
if parent and parent.is_test_directory() or \
- os.path.basename(path) in ('tests', 'legacytests'):
+ self._name in ('tests', 'legacytests'):
self._is_test_dir = True
self._is_external = False
- if parent and parent.is_external() or \
- os.path.basename(path) == 'external':
+ if parent and parent.is_external() or self._name == 'external':
self._is_external = True
self._subdirs = set()
if parent:
parent._subdirs.add(self)
+ self._files = set()
+ self._has_installed_files = None
def set_doc_xml(self, rawdoc, sourcetree):
"""Assiociate Doxygen documentation entity with the directory."""
assert self._rawdoc is None
- assert self._path == rawdoc.get_path().rstrip('/')
+ assert self._abspath == rawdoc.get_path().rstrip('/')
self._rawdoc = rawdoc
def set_module(self, module):
assert self._module is None
self._module = module
+ def add_file(self, fileobj):
+ self._files.add(fileobj)
+
def get_name(self):
return self._name
def get_reporter_location(self):
- return reporter.Location(self._path, None)
+ return reporter.Location(self._abspath, None)
+
+ def get_abspath(self):
+ return self._abspath
- def get_path(self):
- return self._path
+ def get_relpath(self):
+ return self._relpath
def is_test_directory(self):
return self._is_test_dir
def is_external(self):
return self._is_external
+ def has_installed_files(self):
+ if self._has_installed_files is None:
+ self._has_installed_files = False
+ for subdir in self._subdirs:
+ if subdir.has_installed_files():
+ self._has_installed_files = True
+ return True
+ for fileobj in self._files:
+ if fileobj.is_installed():
+ self._has_installed_files = True
+ return True
+ return self._has_installed_files
+
def get_module(self):
if self._module:
return self._module
def get_subdirectories(self):
return self._subdirs
+ def get_files(self):
+ for subdir in self._subdirs:
+ for fileobj in subdir.get_files():
+ yield fileobj
+ for fileobj in self._files:
+ yield fileobj
+
class Module(object):
"""Code module in the GROMACS source tree.
self._name = name
self._rawdoc = None
self._rootdir = rootdir
+ self._group = None
def set_doc_xml(self, rawdoc, sourcetree):
"""Assiociate Doxygen documentation entity with the module."""
assert self._rawdoc is None
self._rawdoc = rawdoc
+ if self._rawdoc.is_documented():
+ groups = list(self._rawdoc.get_groups())
+ if len(groups) == 1:
+ groupname = groups[0].get_name()
+ if groupname.startswith('group_'):
+ self._group = groupname[6:]
def is_documented(self):
return self._rawdoc is not None
def get_name(self):
return self._name
+ def get_root_dir(self):
+ return self._rootdir
+
+ def get_files(self):
+ # TODO: Include public API convenience headers?
+ return self._rootdir.get_files()
+
+ def get_group(self):
+ return self._group
+
+
class Class(object):
"""Class/struct/union in the GROMACS source code."""
def _get_rel_path(self, path):
assert os.path.isabs(path)
if path.startswith(self._build_root):
- return path[len(self._build_root)+1:]
+ return os.path.relpath(path, self._build_root)
if path.startswith(self._source_root):
- return path[len(self._source_root)+1:]
+ return os.path.relpath(path, self._source_root)
raise ValueError("path not under build nor source tree: {0}".format(path))
def _walk_dir(self, rootpath):
assert os.path.isabs(rootpath)
assert rootpath not in self._dirs
relpath = self._get_rel_path(rootpath)
- self._dirs[relpath] = Directory(rootpath, None)
+ self._dirs[relpath] = Directory(rootpath, relpath, None)
for dirpath, dirnames, filenames in os.walk(rootpath):
if 'contrib' in dirnames:
dirnames.remove('contrib')
dirnames.remove(dirname)
continue
relpath = self._get_rel_path(fullpath)
- self._dirs[relpath] = Directory(fullpath, currentdir)
+ self._dirs[relpath] = Directory(fullpath, relpath, currentdir)
extensions = ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu', '.bm')
for filename in filenames:
basename, extension = os.path.splitext(filename)
if extension in extensions:
fullpath = os.path.join(dirpath, filename)
relpath = self._get_rel_path(fullpath)
- self._files[relpath] = File(fullpath, currentdir)
+ self._files[relpath] = File(fullpath, relpath, currentdir)
elif extension == '.cmakein':
extension = os.path.splitext(basename)[1]
if extension in extensions:
fullpath = os.path.join(dirpath, basename)
relpath = self._get_rel_path(fullpath)
fullpath = os.path.join(dirpath, filename)
- self._files[relpath] = GeneratedFile(fullpath, currentdir)
+ self._files[relpath] = GeneratedFile(fullpath, relpath, currentdir)
def _create_module(self, rootdir):
"""Create module for a subdirectory."""
if not fileobj.is_external():
fileobj.scan_contents(self)
- def load_xml(self):
- """Load Doxygen XML information."""
+ def load_xml(self, only_files=False):
+ """Load Doxygen XML information.
+
+ If only_files is True, XML data is not loaded for code constructs, but
+ only for files, directories, and their potential parents.
+ """
xmldir = os.path.join(self._build_root, 'doxygen', 'xml')
self._docset = xml.DocumentationSet(xmldir, self._reporter)
- self._docset.load_details()
- self._docset.merge_duplicates()
+ if only_files:
+ self._docset.load_file_details()
+ else:
+ self._docset.load_details()
+ self._docset.merge_duplicates()
self._load_dirs()
self._load_modules()
self._load_files()
- self._load_classes()
+ if not only_files:
+ self._load_classes()
def _load_dirs(self):
"""Load Doxygen XML directory information."""
relpath = self._get_rel_path(path)
dirobj = self._dirs.get(relpath)
if not dirobj:
- dirobj = Directory(path, parent)
+ dirobj = Directory(path, relpath, parent)
self._dirs[relpath] = dirobj
dirobj.set_doc_xml(dirdoc, self)
self._docmap[dirdoc] = dirobj
relpath = self._get_rel_path(path)
fileobj = self._files.get(relpath)
if not fileobj:
- fileobj = File(path, self._docmap[dirdoc])
+ fileobj = File(path, relpath, self._docmap[dirdoc])
self._files[relpath] = fileobj
fileobj.set_doc_xml(filedoc, self)
self._docmap[filedoc] = fileobj
"""Get iterable for all files in the source tree."""
return self._files.itervalues()
+ def get_modules(self):
+ """Get iterable for all modules in the source tree."""
+ return self._modules.itervalues()
+
def get_classes(self):
"""Get iterable for all classes in the source tree."""
return self._classes
--- /dev/null
+#!/usr/bin/python
+#
+# This file is part of the GROMACS molecular simulation package.
+#
+# Copyright (c) 2012,2013,2014, by the GROMACS development team, led by
+# Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
+# and including many others, as listed in the AUTHORS file in the
+# top-level source directory and at http://www.gromacs.org.
+#
+# GROMACS is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License
+# as published by the Free Software Foundation; either version 2.1
+# of the License, or (at your option) any later version.
+#
+# GROMACS is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with GROMACS; if not, see
+# http://www.gnu.org/licenses, or write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# If you want to redistribute modifications to GROMACS, please
+# consider that scientific software is very special. Version
+# control is crucial - bugs must be traceable. We will be happy to
+# consider code for inclusion in the official distribution, but
+# derived work must not be called official GROMACS. Details are found
+# in the README & COPYING files - if they are missing, get the
+# official version at http://www.gromacs.org.
+#
+# To help us fund GROMACS development, we humbly ask that you cite
+# the research papers on the package. Check out http://www.gromacs.org.
+
+"""Generate include dependency graphs.
+
+This script generates include dependency graphs from the GROMACS source tree.
+One graph is generated to show inter-module dependencies, and separate graphs
+for each module to show file-level dependencies within the module.
+
+Output format for the graphs is suitable for processing with 'dot' in graphviz.
+
+The graphs are built from the source tree representation constructed in
+gmxtree.py.
+
+Classes Graph, Node, Edge, and EdgeType provide a relatively general
+implementation for constructing 'dot' graphs. GraphBuilder is used to
+create Graph instances from a gmxtree.GromacsTree object; the actual graph
+objects will not contain any references to the gmxtree objects.
+
+When run in script mode, the GromacsTree object is first constructed, and then
+GraphBuilder is used to construct the necessary graphs, which are then written
+out.
+
+The produced graphs are documented in doxygen.md.
+"""
+
+import os.path
+import re
+
+from gmxtree import DocType
+
+class EdgeType(object):
+
+ """Enumeration type for edge types in include dependency graphs."""
+
+ # Mapping to string representation for the internal integer values
+ _names = ['test', 'pubimpl', 'libimpl', 'library', 'public',
+ 'intramodule', 'legacy', 'undocumented']
+
+ def __init__(self, value):
+ """Initialize a EdgeType instance.
+
+ EdgeType.{test,pubimpl,...,undocumented} should be used outside the
+ class instead of calling the constructor.
+ """
+ self._value = value
+
+ def __str__(self):
+ """Return string representation for the edge type (for debugging)."""
+ return self._names[self._value]
+
+ def __cmp__(self, other):
+ """Order edge types in the order of increasing coupling."""
+ return cmp(self._value, other._value)
+
+# Tests depend on test
+EdgeType.test = EdgeType(0)
+# Implementation depends on public/library headers
+EdgeType.pubimpl = EdgeType(1)
+EdgeType.libimpl = EdgeType(2)
+# Library header depends on other module
+EdgeType.library = EdgeType(3)
+# Public header depends on other module
+EdgeType.public = EdgeType(4)
+# Intramodule dependency
+EdgeType.intramodule = EdgeType(5)
+EdgeType.legacy = EdgeType(6)
+# Invalid dependency
+EdgeType.undocumented = EdgeType(7)
+
+class Edge(object):
+
+ """Graph edge between two Node objects in 'dot' graph.
+
+ Signifies an include dependency between the two nodes, and manages types
+ associated with the dependencies.
+ """
+
+ def __init__(self, fromnode, tonode, edgetype):
+ """Create edge between given Nodes with given type."""
+ self._fromnode = fromnode
+ self._tonode = tonode
+ self._edgetype = edgetype
+
+ def merge_edge(self, other):
+ """Merge another edge into this one and choose an appropriate type.
+
+ Updates the type of this edge based on the types of the merged edges.
+ """
+ self._edgetype = max(self._edgetype, other._edgetype)
+
+ def format(self):
+ """Format this edge for 'dot'."""
+ if self._fromnode.is_file_node() and self._tonode.is_file_node():
+ properties = ''
+ elif self._edgetype == EdgeType.intramodule:
+ properties = ''
+ elif self._edgetype == EdgeType.test:
+ properties = 'color=".33 .8 .8", style=dashed'
+ elif self._edgetype == EdgeType.libimpl:
+ properties = 'color=".66 .8 .8", style=dashed'
+ elif self._edgetype == EdgeType.pubimpl:
+ properties = 'color=black, style=dashed'
+ elif self._edgetype == EdgeType.library:
+ properties = 'color=".66 .8 .8"'
+ elif self._edgetype == EdgeType.public:
+ properties = 'color=black'
+ elif self._edgetype == EdgeType.legacy:
+ properties = 'color=grey75'
+ else: # undocumented
+ properties = 'color=red'
+ return '{0} -> {1} [{2}]'.format(self._fromnode.get_nodename(),
+ self._tonode.get_nodename(),
+ properties)
+
+class Node(object):
+
+ """Node in 'dot' graph."""
+
+ def __init__(self, nodename, label, style=None, properties=None, is_file=False):
+ """Create node with given attributes.
+
+ is_file does not affect the appearance of the node, but is used for
+ formatting edges between two files differently from other edges.
+ style and properties should be iterables with graphviz attributes for
+ the node.
+
+ Node can have child nodes. Such nodes are rendered as cluster
+ subgraphs for 'dot'.
+ """
+ self._nodename = nodename
+ self._label = label
+ if style:
+ self._style = ','.join(style)
+ else:
+ self._style = None
+ if properties:
+ self._properties = ', '.join(properties)
+ else:
+ self._properties = None
+ self._is_file = is_file
+ self._children = []
+
+ def add_child(self, child):
+ """Add a child node."""
+ self._children.append(child)
+
+ def clear_children(self):
+ """Remove all children from the node."""
+ self._children = []
+
+ def is_file_node(self):
+ """Return True if the node was created with is_file=True."""
+ return self._is_file
+
+ def get_nodename(self):
+ """Get internal name of the node in 'dot'."""
+ return self._nodename
+
+ def get_children(self, recursive=False):
+ """Get list of child nodes."""
+ if recursive:
+ result = list(self._children)
+ for child in self._children:
+ result.extend(child.get_children(recursive=True))
+ return result
+ else:
+ return self._children
+
+ def format(self):
+ """Format this node for 'dot'."""
+ # TODO: Take indent as a parameter to make output marginally nicer.
+ result = ''
+ if self._children:
+ result += ' subgraph cluster_{0} {{\n' \
+ .format(self._nodename)
+ result += ' label = "{0}"\n'.format(self._label)
+ for child in self._children:
+ result += child.format()
+ result += ' }\n'
+ else:
+ properties = 'label="{0}"'.format(self._label)
+ if self._properties:
+ properties += ', ' + self._properties
+ if self._style:
+ properties += ', style="{0}"'.format(self._style)
+ result += ' {0} [{1}]\n'.format(self._nodename, properties)
+ return result
+
+
+class Graph(object):
+
+ """Graph for 'dot'."""
+
+ def __init__(self, nodes, edges):
+ """Create graph with given nodes and edges."""
+ self._nodes = set(nodes)
+ self._edges = edges
+ self._left_to_right = False
+ self._concentrate = True
+
+ def set_options(self, left_to_right=None, concentrate=None):
+ """Set output options for the graph."""
+ if left_to_right != None:
+ self._left_to_right = left_to_right
+ if concentrate != None:
+ self._concentrate = concentrate
+
+ def merge_nodes(self, nodes, target):
+ """Merge a set of nodes into a single node.
+
+ All nodes from the list nodes are merged into the target node.
+ All edges to or from the merged nodes are rerouted to/from target
+ instead. Duplicate edges are not created. Instead, if an edge already
+ exists, the edge types are merged. All nodes from the list nodes are
+ removed from the graph after the merge is done.
+ """
+ nodes = set(nodes)
+ nodes.add(target)
+ newedges = []
+ edgesto = dict()
+ edgesfrom = dict()
+ for edge in self._edges:
+ isfrom = (edge._fromnode in nodes)
+ isto = (edge._tonode in nodes)
+ if isfrom and isto:
+ pass
+ elif isfrom:
+ if not edge._tonode in edgesfrom:
+ edgesfrom[edge._tonode] = \
+ Edge(target, edge._tonode, edge._edgetype)
+ else:
+ edgesfrom[edge._tonode].merge_edge(edge)
+ elif isto:
+ if not edge._fromnode in edgesto:
+ edgesto[edge._fromnode] = \
+ Edge(edge._fromnode, target, edge._edgetype)
+ else:
+ edgesto[edge._fromnode].merge_edge(edge)
+ else:
+ newedges.append(edge)
+ newedges.extend(edgesfrom.values())
+ newedges.extend(edgesto.values())
+ self._edges = newedges
+
+ def collapse_node(self, node):
+ """Merge all children of a node into the node.
+
+ All child nodes are removed after the merge is done.
+ """
+ nodes = node.get_children(recursive=True)
+ self.merge_nodes(nodes, node)
+ node.clear_children()
+
+ def write(self, outfile):
+ """Write the graph in 'dot' format."""
+ outfile.write('digraph includedeps {\n')
+ if self._left_to_right:
+ outfile.write(' rankdir = LR\n')
+ if self._concentrate:
+ outfile.write(' concentrate = true\n')
+ outfile.write(' node [fontname="FreeSans",fontsize=10,height=.2,'
+ 'shape=box]\n')
+ for node in self._nodes:
+ outfile.write(node.format())
+ for edge in self._edges:
+ outfile.write(' ' + edge.format() + '\n')
+ outfile.write('}\n')
+
+class GraphBuilder(object):
+
+ """Builder for Graph objects from gmxtree.GromacsTree representation."""
+
+ def __init__(self, tree):
+ """Initialize builder for a given tree representation."""
+ self._tree = tree
+
+ def _create_file_node(self, fileobj, filenodes):
+ """Create graph node for a file object.
+
+ filenodes is a dict() that maps file objects to their nodes, and is
+ updated by this call.
+ """
+ nodename = re.subn(r'[-./]', '_', fileobj.get_relpath())[0]
+ style = []
+ properties = []
+ properties.append('URL="\\ref {0}"'.format(fileobj.get_name()))
+ if not fileobj.get_module():
+ style.append('bold')
+ properties.append('color=red')
+ if fileobj.is_test_file():
+ style.append('filled')
+ properties.append('fillcolor=".33 .2 1"')
+ elif fileobj.is_source_file():
+ style.append('filled')
+ properties.append('fillcolor=grey75')
+ elif fileobj.get_api_type() == DocType.public:
+ style.append('filled')
+ properties.append('fillcolor=".66 .2 1"')
+ elif fileobj.get_api_type() == DocType.library:
+ style.append('filled')
+ properties.append('fillcolor=".66 .5 1"')
+ node = Node(nodename, fileobj.get_name(), style, properties, is_file=True)
+ filenodes[fileobj] = node
+ return node
+
+ def _create_file_edge(self, fromfile, tofile, filenodes):
+ """Create edge between two file objects.
+
+ Determines the type for the edge from the information provided by
+ gmxtree.
+ """
+ intramodule = (fromfile.get_module() == tofile.get_module())
+ is_legacy = not tofile.get_module().is_documented()
+ if fromfile.get_module() == tofile.get_module():
+ edgetype = EdgeType.intramodule
+ elif tofile.get_api_type() == DocType.internal:
+ if is_legacy:
+ edgetype = EdgeType.legacy
+ else:
+ edgetype = EdgeType.undocumented
+ elif fromfile.is_test_file():
+ edgetype = EdgeType.test
+ elif tofile.is_test_file():
+ edgetype = EdgeType.undocumented
+ elif fromfile.is_source_file() or \
+ (fromfile.get_api_type() <= DocType.internal and \
+ not fromfile.is_installed()):
+ if tofile.get_api_type() == DocType.public:
+ edgetype = EdgeType.pubimpl
+ elif tofile.get_api_type() == DocType.library:
+ edgetype = EdgeType.libimpl
+ elif is_legacy or not tofile.is_documented():
+ edgetype = EdgeType.legacy
+ else:
+ raise ValueError('Unknown edge type between {0} and {1}'
+ .format(fromfile.path, tofile.path))
+ elif fromfile.get_api_type() == DocType.library:
+ edgetype = EdgeType.library
+ elif fromfile.get_api_type() == DocType.public or fromfile.is_installed():
+ if tofile.get_api_type() == DocType.public or \
+ tofile.get_documentation_type() == DocType.public or \
+ (tofile.is_installed() and not tofile.is_documented()):
+ edgetype = EdgeType.public
+ else:
+ edgetype = EdgeType.undocumented
+ else:
+ raise ValueError('Unknown edge type between {0} and {1}'
+ .format(fromfile.path, tofile.path))
+ return Edge(filenodes[fromfile], filenodes[tofile], edgetype)
+
+ def _create_file_edges(self, filenodes):
+ """Create edges between all file nodes.
+
+ Create edges between file nodes specified in filenodes from all include
+ dependencies. An edge is created only if both ends of the dependency
+ are in the list of nodes.
+ """
+ edges = []
+ for fileobj in filenodes.iterkeys():
+ for includedfile in fileobj.get_includes():
+ otherfile = includedfile.get_file()
+ if otherfile and otherfile in filenodes:
+ edge = self._create_file_edge(fileobj, otherfile, filenodes)
+ edges.append(edge)
+ return edges
+
+ def _create_module_node(self, module, filenodes):
+ """Create node for a module.
+
+ The created node will have all files in the module as its child nodes.
+ All created file nodes are added to the filenodes dict.
+ """
+ style = []
+ properties = []
+ properties.append('shape=ellipse')
+ properties.append('URL="\\ref module_{0}"'.format(module.get_name()))
+ if not module.is_documented():
+ style.append('filled')
+ properties.append('fillcolor=grey75')
+ elif module.get_group() == 'analysismodules':
+ style.append('filled')
+ properties.append('fillcolor="0 .2 1"')
+ elif module.get_group() == 'utilitymodules':
+ style.append('filled')
+ properties.append('fillcolor=".08 .2 1"')
+ elif module.get_group() == 'mdrun':
+ style.append('filled')
+ properties.append('fillcolor=".75 .2 1"')
+ rootdir = module.get_root_dir()
+ if rootdir.has_installed_files():
+ properties.append('color=".66 .5 1"')
+ properties.append('penwidth=3')
+ nodename = 'module_' + re.subn(r'[-./]', '_', rootdir.get_relpath())[0]
+ label = module.get_name()[7:]
+ node = Node(nodename, label, style, properties)
+ for childfile in module.get_files():
+ node.add_child(self._create_file_node(childfile, filenodes))
+ return node
+
+ def create_modules_graph(self):
+ """Create module dependency graph."""
+ filenodes = dict()
+ nodes = []
+ modulenodes = []
+ libgromacsnode = Node('libgromacs', 'libgromacs')
+ nodes.append(libgromacsnode)
+ for moduleobj in self._tree.get_modules():
+ node = self._create_module_node(moduleobj, filenodes)
+ if moduleobj.get_root_dir().get_relpath().startswith('src/gromacs'):
+ libgromacsnode.add_child(node)
+ else:
+ nodes.append(node)
+ modulenodes.append(node)
+ edges = self._create_file_edges(filenodes)
+ graph = Graph(nodes, edges)
+ for node in modulenodes:
+ graph.collapse_node(node)
+ graph.set_options(concentrate=False)
+ return graph
+
+ def create_module_file_graph(self, module):
+ """Create file dependency graph for files within a module."""
+ filenodes = dict()
+ nodes = []
+ for fileobj in module.get_files():
+ nodes.append(self._create_file_node(fileobj, filenodes))
+ edges = self._create_file_edges(filenodes)
+ graph = Graph(nodes, edges)
+ graph.set_options(left_to_right=True)
+ return graph
+
+def main():
+ """Run the graph generation script."""
+ import os
+ import sys
+
+ from optparse import OptionParser
+
+ from gmxtree import GromacsTree
+ from reporter import Reporter
+
+ parser = OptionParser()
+ parser.add_option('-S', '--source-root',
+ help='Source tree root directory')
+ parser.add_option('-B', '--build-root',
+ help='Build tree root directory')
+ parser.add_option('--installed',
+ help='Read list of installed files from given file')
+ parser.add_option('-o', '--outdir', default='.',
+ help='Specify output directory for graphs')
+ parser.add_option('-q', '--quiet', action='store_true',
+ help='Do not write status messages')
+ options, args = parser.parse_args()
+
+ installedlist = []
+ if options.installed:
+ with open(options.installed, 'r') as outfile:
+ for line in outfile:
+ installedlist.append(line.strip())
+
+ reporter = Reporter(quiet=True)
+
+ if not options.quiet:
+ sys.stderr.write('Scanning source tree...\n')
+ tree = GromacsTree(options.source_root, options.build_root, reporter)
+ tree.set_installed_file_list(installedlist)
+ if not options.quiet:
+ sys.stderr.write('Reading source files...\n')
+ tree.scan_files()
+ if not options.quiet:
+ sys.stderr.write('Reading Doxygen XML files...\n')
+ tree.load_xml(only_files=True)
+
+ if not options.quiet:
+ sys.stderr.write('Writing graphs...\n')
+ graphbuilder = GraphBuilder(tree)
+ if not os.path.exists(options.outdir):
+ os.mkdir(options.outdir)
+
+ filename = os.path.join(options.outdir, 'module-deps.dot')
+ graph = graphbuilder.create_modules_graph()
+ with open(filename, 'w') as outfile:
+ graph.write(outfile)
+
+ # Skip some modules that are too big to make any sense
+ skippedmodules = ('legacyheaders', 'gmxlib', 'mdlib', 'gmxana', 'gmxpreprocess')
+ for module in tree.get_modules():
+ if not module.get_name()[7:] in skippedmodules:
+ filename = '{0}-deps.dot'.format(module.get_name())
+ filename = os.path.join(options.outdir, filename)
+ graph = graphbuilder.create_module_file_graph(module)
+ with open(filename, 'w') as outfile:
+ graph.write(outfile)
+
+if __name__ == '__main__':
+ main()
"""Collect and write out issues found by checker scripts."""
- def __init__(self, logfile=None):
+ def __init__(self, logfile=None, quiet=False):
"""Initialize the reporter.
If logfile is set to a file name, all issues will be written to this
file in addition to stderr.
+
+ If quiet is set to True, the reporter will suppress all output.
"""
self._logfp = None
if logfile:
self._logfp = open(logfile, 'w')
self._messages = []
self._filters = []
+ self._quiet = quiet
def _write(self, message):
"""Implement actual message writing."""
def _report(self, message):
"""Handle a single reporter message."""
+ if self._quiet:
+ return
for filterobj in self._filters:
if filterobj.matches(message):
return