3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 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 """GROMACS-specific representation for source tree and documentation.
38 This module provides classes that construct a GROMACS-specific representation
39 of the source tree and associate the Doxygen XML output with it. It constructs
40 an initial representation by walking the source tree in the file system, and
41 then associates information from the Doxygen XML output into this.
42 It also adds some additional knowledge from how the GROMACS source tree is
43 organized to construct a representation that is easy to process and check as
44 the top-level scripts expect.
46 The object model is rooted at a GromacsTree object. Currently, it constructs a
47 representation of the source tree from the file system, but is otherwise mostly
48 a thin wrapper around the Doxygen XML tree. It already adds some relations and
49 rules that come from GROMACS-specific knowledge. In the future, more such
50 customizations will be added.
57 import doxygenxml as xml
59 # We import DocType directly so that it is exposed from this module as well.
60 from doxygenxml import DocType
62 def _get_api_type_for_compound(grouplist):
63 """Helper function to deduce API type from Doxygen group membership."""
64 result = DocType.internal
65 for group in grouplist:
66 if isinstance(group, xml.Group):
67 if group.get_name() == 'group_publicapi':
68 result = DocType.public
69 elif group.get_name() == 'group_libraryapi':
70 result = DocType.library
71 # TODO: Check for multiple group membership
74 class IncludedFile(object):
76 """Information about an #include directive in a file."""
78 def __init__(self, path, lineno, included_file, included_path, is_relative, is_system):
80 self._line_number = lineno
81 self._included_file = included_file
82 self._included_path = included_path
83 #self._used_include_path = used_include_path
84 self._is_relative = is_relative
85 self._is_system = is_system
89 return '<{0}>'.format(self._included_path)
91 return '"{0}"'.format(self._included_path)
94 return self._is_system
96 def is_relative(self):
97 return self._is_relative
100 return self._included_file
102 def get_reporter_location(self):
103 return reporter.Location(self._path, self._line_number)
107 """Source/header file in the GROMACS tree."""
109 def __init__(self, path, directory):
110 """Initialize a file representation with basic information."""
112 self._dir = directory
114 self._installed = False
115 extension = os.path.splitext(path)[1]
116 self._sourcefile = (extension in ('.c', '.cc', '.cpp', '.cu'))
117 self._apitype = DocType.none
118 self._modules = set()
121 def set_doc_xml(self, rawdoc, sourcetree):
122 """Assiociate Doxygen documentation entity with the file."""
123 assert self._rawdoc is None
124 assert rawdoc.is_source_file() == self._sourcefile
125 self._rawdoc = rawdoc
126 if self._rawdoc.is_documented():
127 grouplist = self._rawdoc.get_groups()
128 self._apitype = _get_api_type_for_compound(grouplist)
129 for group in grouplist:
130 module = sourcetree.get_object(group)
132 self._modules.add(module)
134 def set_installed(self):
135 """Mark the file installed."""
136 self._installed = True
138 def _process_include(self, lineno, is_system, includedpath, sourcetree):
139 """Process #include directive during scan()."""
142 fileobj = sourcetree.find_include_file(includedpath)
144 fullpath = os.path.join(self._dir.get_path(), includedpath)
145 fullpath = os.path.abspath(fullpath)
146 if os.path.exists(fullpath):
148 fileobj = sourcetree.get_file(fullpath)
150 fileobj = sourcetree.find_include_file(includedpath)
151 self._includes.append(IncludedFile(self.get_path(), lineno, fileobj, includedpath,
152 is_relative, is_system))
154 def scan_contents(self, sourcetree):
155 """Scan the file contents and initialize information based on it."""
156 # TODO: Consider a more robust regex.
157 include_re = r'^#\s*include\s+(?P<quote>["<])(?P<path>[^">]*)[">]'
158 with open(self._path, 'r') as scanfile:
159 for lineno, line in enumerate(scanfile, 1):
160 match = re.match(include_re, line)
162 is_system = (match.group('quote') == '<')
163 includedpath = match.group('path')
164 self._process_include(lineno, is_system, includedpath,
167 def get_reporter_location(self):
168 return reporter.Location(self._path, None)
170 def is_installed(self):
171 return self._installed
173 def is_external(self):
174 return self._dir.is_external()
176 def is_source_file(self):
177 return self._sourcefile
179 def is_test_file(self):
180 return self._dir.is_test_directory()
182 def is_documented(self):
183 return self._rawdoc and self._rawdoc.is_documented()
185 def has_brief_description(self):
186 return self._rawdoc and self._rawdoc.has_brief_description()
191 def get_documentation_type(self):
194 return self._rawdoc.get_visibility()
196 def get_api_type(self):
199 def get_expected_module(self):
200 return self._dir.get_module()
202 def get_doc_modules(self):
205 def get_module(self):
206 module = self.get_expected_module()
207 if not module and len(self._modules) == 1:
208 module = list(self._modules)[0]
211 def get_includes(self):
212 return self._includes
214 class GeneratedFile(File):
217 class Directory(object):
219 """(Sub)directory in the GROMACS tree."""
221 def __init__(self, path, parent):
222 """Initialize a file representation with basic information."""
224 self._name = os.path.basename(path)
225 self._parent = parent
228 self._is_test_dir = False
229 if parent and parent.is_test_directory() or \
230 os.path.basename(path) in ('tests', 'legacytests'):
231 self._is_test_dir = True
232 self._is_external = False
233 if parent and parent.is_external() or \
234 os.path.basename(path) == 'external':
235 self._is_external = True
236 self._subdirs = set()
238 parent._subdirs.add(self)
240 def set_doc_xml(self, rawdoc, sourcetree):
241 """Assiociate Doxygen documentation entity with the directory."""
242 assert self._rawdoc is None
243 assert self._path == rawdoc.get_path().rstrip('/')
244 self._rawdoc = rawdoc
246 def set_module(self, module):
247 assert self._module is None
248 self._module = module
253 def get_reporter_location(self):
254 return reporter.Location(self._path, None)
259 def is_test_directory(self):
260 return self._is_test_dir
262 def is_external(self):
263 return self._is_external
265 def get_module(self):
269 return self._parent.get_module()
272 def get_subdirectories(self):
275 class Module(object):
277 """Code module in the GROMACS source tree.
279 Modules are specific subdirectories that host a more or less coherent
280 set of routines. Simplified, every subdirectory under src/gromacs/ is
281 a different module. This object provides that abstraction and also links
282 the subdirectory to the module documentation (documented as a group in
283 Doxygen) if that exists.
286 def __init__(self, name, rootdir):
289 self._rootdir = rootdir
291 def set_doc_xml(self, rawdoc, sourcetree):
292 """Assiociate Doxygen documentation entity with the module."""
293 assert self._rawdoc is None
294 self._rawdoc = rawdoc
296 def is_documented(self):
297 return self._rawdoc is not None
304 """Class/struct/union in the GROMACS source code."""
306 def __init__(self, rawdoc, files):
307 self._rawdoc = rawdoc
308 self._files = set(files)
311 return self._rawdoc.get_name()
313 def get_reporter_location(self):
314 return self._rawdoc.get_reporter_location()
319 def is_documented(self):
320 return self._rawdoc.is_documented()
322 def has_brief_description(self):
323 return self._rawdoc.has_brief_description()
325 def get_documentation_type(self):
326 if not self.is_documented():
328 if self._rawdoc.is_local():
329 return DocType.internal
330 return self._rawdoc.get_visibility()
332 def get_file_documentation_type(self):
333 return max([fileobj.get_documentation_type() for fileobj in self._files])
335 def is_in_installed_file(self):
336 return any([fileobj.is_installed() for fileobj in self._files])
338 class GromacsTree(object):
340 """Root object for navigating the GROMACS source tree.
342 On initialization, the list of files and directories is initialized by
343 walking the source tree, and modules are created for top-level
344 subdirectories. At this point, only information that is accessible from
345 file names and paths only is available.
347 set_installed_file_list() can be called to set the list of installed
350 scan_files() can be called to read all the files and initialize #include
351 dependencies between the files based on the information. This is done like
352 this instead of relying on Doxygen-extracted include files to make the
353 dependency graph independent from preprocessor macro definitions
354 (Doxygen only sees those #includes that the preprocessor sees, which
355 depends on what #defines it has seen).
357 load_xml() can be called to load information from Doxygen XML data in
358 the build tree (the Doxygen XML data must have been built separately).
361 def __init__(self, source_root, build_root, reporter):
362 """Initialize the tree object by walking the source tree."""
363 self._source_root = os.path.abspath(source_root)
364 self._build_root = os.path.abspath(build_root)
365 self._reporter = reporter
367 self._docmap = dict()
370 self._modules = dict()
371 self._classes = set()
372 self._walk_dir(os.path.join(self._source_root, 'src'))
373 rootdir = self._get_dir(os.path.join('src', 'gromacs'))
374 for subdir in rootdir.get_subdirectories():
375 self._create_module(subdir)
376 rootdir = self._get_dir(os.path.join('src', 'testutils'))
377 self._create_module(rootdir)
379 def _get_rel_path(self, path):
380 assert os.path.isabs(path)
381 if path.startswith(self._build_root):
382 return path[len(self._build_root)+1:]
383 if path.startswith(self._source_root):
384 return path[len(self._source_root)+1:]
385 raise ValueError("path not under build nor source tree: {0}".format(path))
387 def _walk_dir(self, rootpath):
388 """Construct representation of the source tree by walking the file system."""
389 assert os.path.isabs(rootpath)
390 assert rootpath not in self._dirs
391 relpath = self._get_rel_path(rootpath)
392 self._dirs[relpath] = Directory(rootpath, None)
393 for dirpath, dirnames, filenames in os.walk(rootpath):
394 if 'contrib' in dirnames:
395 dirnames.remove('contrib')
396 if 'refdata' in dirnames:
397 dirnames.remove('refdata')
398 currentdir = self._dirs[self._get_rel_path(dirpath)]
399 # Loop through a copy so that we can modify dirnames.
400 for dirname in list(dirnames):
401 fullpath = os.path.join(dirpath, dirname)
402 if fullpath == self._build_root:
403 dirnames.remove(dirname)
405 relpath = self._get_rel_path(fullpath)
406 self._dirs[relpath] = Directory(fullpath, currentdir)
407 extensions = ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu', '.bm')
408 for filename in filenames:
409 basename, extension = os.path.splitext(filename)
410 if extension in extensions:
411 fullpath = os.path.join(dirpath, filename)
412 relpath = self._get_rel_path(fullpath)
413 self._files[relpath] = File(fullpath, currentdir)
414 elif extension == '.cmakein':
415 extension = os.path.splitext(basename)[1]
416 if extension in extensions:
417 fullpath = os.path.join(dirpath, basename)
418 relpath = self._get_rel_path(fullpath)
419 fullpath = os.path.join(dirpath, filename)
420 self._files[relpath] = GeneratedFile(fullpath, currentdir)
422 def _create_module(self, rootdir):
423 """Create module for a subdirectory."""
424 name = 'module_' + rootdir.get_name()
425 moduleobj = Module(name, rootdir)
426 rootdir.set_module(moduleobj)
427 self._modules[name] = moduleobj
429 def scan_files(self):
430 """Read source files to initialize #include dependencies."""
431 for fileobj in self._files.itervalues():
432 if not fileobj.is_external():
433 fileobj.scan_contents(self)
436 """Load Doxygen XML information."""
437 xmldir = os.path.join(self._build_root, 'doxygen', 'xml')
438 self._docset = xml.DocumentationSet(xmldir, self._reporter)
439 self._docset.load_details()
440 self._docset.merge_duplicates()
446 def _load_dirs(self):
447 """Load Doxygen XML directory information."""
448 rootdirs = self._docset.get_compounds(xml.Directory,
449 lambda x: x.get_parent() is None)
450 for dirdoc in rootdirs:
451 self._load_dir(dirdoc, None)
453 def _load_dir(self, dirdoc, parent):
454 """Load Doxygen XML directory information for a single directory."""
455 path = dirdoc.get_path().rstrip('/')
456 if not os.path.isabs(path):
457 self._reporter.xml_assert(dirdoc.get_xml_path(),
458 "expected absolute path in Doxygen-produced XML file")
460 relpath = self._get_rel_path(path)
461 dirobj = self._dirs.get(relpath)
463 dirobj = Directory(path, parent)
464 self._dirs[relpath] = dirobj
465 dirobj.set_doc_xml(dirdoc, self)
466 self._docmap[dirdoc] = dirobj
467 for subdirdoc in dirdoc.get_subdirectories():
468 self._load_dir(subdirdoc, dirobj)
470 def _load_modules(self):
471 """Load Doxygen XML module (group) information."""
472 moduledocs = self._docset.get_compounds(xml.Group,
473 lambda x: x.get_name().startswith('module_'))
474 for moduledoc in moduledocs:
475 moduleobj = self._modules.get(moduledoc.get_name())
477 self._reporter.input_error(
478 "no matching directory for module: {0}".format(moduledoc))
480 moduleobj.set_doc_xml(moduledoc, self)
481 self._docmap[moduledoc] = moduleobj
483 def _load_files(self):
484 """Load Doxygen XML file information."""
485 for filedoc in self._docset.get_files():
486 path = filedoc.get_path()
487 if not os.path.isabs(path):
488 self._reporter.xml_assert(filedoc.get_xml_path(),
489 "expected absolute path in Doxygen-produced XML file")
491 extension = os.path.splitext(filedoc.get_path())[1]
492 # We don't care about Markdown files that only produce pages
493 # (and fail the directory check below).
494 if extension == '.md':
496 dirdoc = filedoc.get_directory()
498 self._reporter.xml_assert(filedoc.get_xml_path(),
499 "file is not in any directory in Doxygen")
501 relpath = self._get_rel_path(path)
502 fileobj = self._files.get(relpath)
504 fileobj = File(path, self._docmap[dirdoc])
505 self._files[relpath] = fileobj
506 fileobj.set_doc_xml(filedoc, self)
507 self._docmap[filedoc] = fileobj
509 def _load_classes(self):
510 """Load Doxygen XML class information."""
511 classdocs = self._docset.get_classes()
512 for classdoc in classdocs:
513 files = [self._docmap[filedoc] for filedoc in classdoc.get_files()]
514 classobj = Class(classdoc, files)
515 self._docmap[classdoc] = classobj
516 self._classes.add(classobj)
518 def _get_dir(self, relpath):
519 """Get directory object for a path relative to source tree root."""
520 return self._dirs.get(relpath)
522 def get_file(self, path):
523 """Get file object for a path relative to source tree root."""
524 return self._files.get(self._get_rel_path(path))
526 def find_include_file(self, includedpath):
527 """Find a file object corresponding to an include path."""
528 for testdir in ('src', 'src/gromacs/legacyheaders', 'src/external/thread_mpi/include'):
529 testpath = os.path.join(testdir, includedpath)
530 if testpath in self._files:
531 return self._files[testpath]
533 def set_installed_file_list(self, installedfiles):
534 """Set list of installed files."""
535 for path in installedfiles:
536 if not os.path.isabs(path):
537 self._reporter.input_error(
538 "installed file not specified with absolute path: {0}"
541 relpath = self._get_rel_path(path)
542 if relpath not in self._files:
543 self._reporter.input_error(
544 "installed file not in source tree: {0}".format(path))
546 self._files[relpath].set_installed()
548 def get_object(self, docobj):
549 """Get tree object for a Doxygen XML object."""
550 return self._docmap.get(docobj)
553 """Get iterable for all files in the source tree."""
554 return self._files.itervalues()
556 def get_classes(self):
557 """Get iterable for all classes in the source tree."""
560 def get_members(self):
561 """Get iterable for all members (in Doxygen terms) in the source tree."""
562 # TODO: Add wrappers to solve some issues.
563 return self._docset.get_members()