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
, abspath
, lineno
, included_file
, included_path
, is_relative
, is_system
):
79 self
._abspath
= abspath
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
._abspath
, self
._line
_number
)
107 """Source/header file in the GROMACS tree."""
109 def __init__(self
, abspath
, relpath
, directory
):
110 """Initialize a file representation with basic information."""
111 self
._abspath
= abspath
112 self
._relpath
= relpath
113 self
._dir
= directory
115 self
._installed
= False
116 extension
= os
.path
.splitext(abspath
)[1]
117 self
._sourcefile
= (extension
in ('.c', '.cc', '.cpp', '.cu'))
118 self
._apitype
= DocType
.none
119 self
._modules
= set()
121 directory
.add_file(self
)
123 def set_doc_xml(self
, rawdoc
, sourcetree
):
124 """Assiociate Doxygen documentation entity with the file."""
125 assert self
._rawdoc
is None
126 assert rawdoc
.is_source_file() == self
._sourcefile
127 self
._rawdoc
= rawdoc
128 if self
._rawdoc
.is_documented():
129 grouplist
= self
._rawdoc
.get_groups()
130 self
._apitype
= _get_api_type_for_compound(grouplist
)
131 for group
in grouplist
:
132 module
= sourcetree
.get_object(group
)
134 self
._modules
.add(module
)
136 def set_installed(self
):
137 """Mark the file installed."""
138 self
._installed
= True
140 def _process_include(self
, lineno
, is_system
, includedpath
, sourcetree
):
141 """Process #include directive during scan()."""
144 fileobj
= sourcetree
.find_include_file(includedpath
)
146 fullpath
= os
.path
.join(self
._dir
.get_abspath(), includedpath
)
147 fullpath
= os
.path
.abspath(fullpath
)
148 if os
.path
.exists(fullpath
):
150 fileobj
= sourcetree
.get_file(fullpath
)
152 fileobj
= sourcetree
.find_include_file(includedpath
)
153 self
._includes
.append(IncludedFile(self
.get_abspath(), lineno
, fileobj
, includedpath
,
154 is_relative
, is_system
))
156 def scan_contents(self
, sourcetree
):
157 """Scan the file contents and initialize information based on it."""
158 # TODO: Consider a more robust regex.
159 include_re
= r
'^#\s*include\s+(?P<quote>["<])(?P<path>[^">]*)[">]'
160 with
open(self
._abspath
, 'r') as scanfile
:
161 for lineno
, line
in enumerate(scanfile
, 1):
162 match
= re
.match(include_re
, line
)
164 is_system
= (match
.group('quote') == '<')
165 includedpath
= match
.group('path')
166 self
._process
_include
(lineno
, is_system
, includedpath
,
169 def get_reporter_location(self
):
170 return reporter
.Location(self
._abspath
, None)
172 def is_installed(self
):
173 return self
._installed
175 def is_external(self
):
176 return self
._dir
.is_external()
178 def is_source_file(self
):
179 return self
._sourcefile
181 def is_test_file(self
):
182 return self
._dir
.is_test_directory()
184 def is_documented(self
):
185 return self
._rawdoc
and self
._rawdoc
.is_documented()
187 def has_brief_description(self
):
188 return self
._rawdoc
and self
._rawdoc
.has_brief_description()
190 def get_abspath(self
):
193 def get_relpath(self
):
197 return os
.path
.basename(self
._abspath
)
199 def get_documentation_type(self
):
202 return self
._rawdoc
.get_visibility()
204 def get_api_type(self
):
207 def get_expected_module(self
):
208 return self
._dir
.get_module()
210 def get_doc_modules(self
):
213 def get_module(self
):
214 module
= self
.get_expected_module()
215 if not module
and len(self
._modules
) == 1:
216 module
= list(self
._modules
)[0]
219 def get_includes(self
):
220 return self
._includes
222 class GeneratedFile(File
):
225 class Directory(object):
227 """(Sub)directory in the GROMACS tree."""
229 def __init__(self
, abspath
, relpath
, parent
):
230 """Initialize a file representation with basic information."""
231 self
._abspath
= abspath
232 self
._relpath
= relpath
233 self
._name
= os
.path
.basename(abspath
)
234 self
._parent
= parent
237 self
._is
_test
_dir
= False
238 if parent
and parent
.is_test_directory() or \
239 self
._name
in ('tests', 'legacytests'):
240 self
._is
_test
_dir
= True
241 self
._is
_external
= False
242 if parent
and parent
.is_external() or self
._name
== 'external':
243 self
._is
_external
= True
244 self
._subdirs
= set()
246 parent
._subdirs
.add(self
)
248 self
._has
_installed
_files
= None
250 def set_doc_xml(self
, rawdoc
, sourcetree
):
251 """Assiociate Doxygen documentation entity with the directory."""
252 assert self
._rawdoc
is None
253 assert self
._abspath
== rawdoc
.get_path().rstrip('/')
254 self
._rawdoc
= rawdoc
256 def set_module(self
, module
):
257 assert self
._module
is None
258 self
._module
= module
260 def add_file(self
, fileobj
):
261 self
._files
.add(fileobj
)
266 def get_reporter_location(self
):
267 return reporter
.Location(self
._abspath
, None)
269 def get_abspath(self
):
272 def get_relpath(self
):
275 def is_test_directory(self
):
276 return self
._is
_test
_dir
278 def is_external(self
):
279 return self
._is
_external
281 def has_installed_files(self
):
282 if self
._has
_installed
_files
is None:
283 self
._has
_installed
_files
= False
284 for subdir
in self
._subdirs
:
285 if subdir
.has_installed_files():
286 self
._has
_installed
_files
= True
288 for fileobj
in self
._files
:
289 if fileobj
.is_installed():
290 self
._has
_installed
_files
= True
292 return self
._has
_installed
_files
294 def get_module(self
):
298 return self
._parent
.get_module()
301 def get_subdirectories(self
):
305 for subdir
in self
._subdirs
:
306 for fileobj
in subdir
.get_files():
308 for fileobj
in self
._files
:
311 class Module(object):
313 """Code module in the GROMACS source tree.
315 Modules are specific subdirectories that host a more or less coherent
316 set of routines. Simplified, every subdirectory under src/gromacs/ is
317 a different module. This object provides that abstraction and also links
318 the subdirectory to the module documentation (documented as a group in
319 Doxygen) if that exists.
322 def __init__(self
, name
, rootdir
):
325 self
._rootdir
= rootdir
328 def set_doc_xml(self
, rawdoc
, sourcetree
):
329 """Assiociate Doxygen documentation entity with the module."""
330 assert self
._rawdoc
is None
331 self
._rawdoc
= rawdoc
332 if self
._rawdoc
.is_documented():
333 groups
= list(self
._rawdoc
.get_groups())
335 groupname
= groups
[0].get_name()
336 if groupname
.startswith('group_'):
337 self
._group
= groupname
[6:]
339 def is_documented(self
):
340 return self
._rawdoc
is not None
345 def get_root_dir(self
):
349 # TODO: Include public API convenience headers?
350 return self
._rootdir
.get_files()
358 """Class/struct/union in the GROMACS source code."""
360 def __init__(self
, rawdoc
, files
):
361 self
._rawdoc
= rawdoc
362 self
._files
= set(files
)
365 return self
._rawdoc
.get_name()
367 def get_reporter_location(self
):
368 return self
._rawdoc
.get_reporter_location()
373 def is_documented(self
):
374 return self
._rawdoc
.is_documented()
376 def has_brief_description(self
):
377 return self
._rawdoc
.has_brief_description()
379 def get_documentation_type(self
):
380 if not self
.is_documented():
382 if self
._rawdoc
.is_local():
383 return DocType
.internal
384 return self
._rawdoc
.get_visibility()
386 def get_file_documentation_type(self
):
387 return max([fileobj
.get_documentation_type() for fileobj
in self
._files
])
389 def is_in_installed_file(self
):
390 return any([fileobj
.is_installed() for fileobj
in self
._files
])
392 class GromacsTree(object):
394 """Root object for navigating the GROMACS source tree.
396 On initialization, the list of files and directories is initialized by
397 walking the source tree, and modules are created for top-level
398 subdirectories. At this point, only information that is accessible from
399 file names and paths only is available.
401 set_installed_file_list() can be called to set the list of installed
404 scan_files() can be called to read all the files and initialize #include
405 dependencies between the files based on the information. This is done like
406 this instead of relying on Doxygen-extracted include files to make the
407 dependency graph independent from preprocessor macro definitions
408 (Doxygen only sees those #includes that the preprocessor sees, which
409 depends on what #defines it has seen).
411 load_xml() can be called to load information from Doxygen XML data in
412 the build tree (the Doxygen XML data must have been built separately).
415 def __init__(self
, source_root
, build_root
, reporter
):
416 """Initialize the tree object by walking the source tree."""
417 self
._source
_root
= os
.path
.abspath(source_root
)
418 self
._build
_root
= os
.path
.abspath(build_root
)
419 self
._reporter
= reporter
421 self
._docmap
= dict()
424 self
._modules
= dict()
425 self
._classes
= set()
426 self
._walk
_dir
(os
.path
.join(self
._source
_root
, 'src'))
427 rootdir
= self
._get
_dir
(os
.path
.join('src', 'gromacs'))
428 for subdir
in rootdir
.get_subdirectories():
429 self
._create
_module
(subdir
)
430 rootdir
= self
._get
_dir
(os
.path
.join('src', 'testutils'))
431 self
._create
_module
(rootdir
)
433 def _get_rel_path(self
, path
):
434 assert os
.path
.isabs(path
)
435 if path
.startswith(self
._build
_root
):
436 return os
.path
.relpath(path
, self
._build
_root
)
437 if path
.startswith(self
._source
_root
):
438 return os
.path
.relpath(path
, self
._source
_root
)
439 raise ValueError("path not under build nor source tree: {0}".format(path
))
441 def _walk_dir(self
, rootpath
):
442 """Construct representation of the source tree by walking the file system."""
443 assert os
.path
.isabs(rootpath
)
444 assert rootpath
not in self
._dirs
445 relpath
= self
._get
_rel
_path
(rootpath
)
446 self
._dirs
[relpath
] = Directory(rootpath
, relpath
, None)
447 for dirpath
, dirnames
, filenames
in os
.walk(rootpath
):
448 if 'contrib' in dirnames
:
449 dirnames
.remove('contrib')
450 if 'refdata' in dirnames
:
451 dirnames
.remove('refdata')
452 currentdir
= self
._dirs
[self
._get
_rel
_path
(dirpath
)]
453 # Loop through a copy so that we can modify dirnames.
454 for dirname
in list(dirnames
):
455 fullpath
= os
.path
.join(dirpath
, dirname
)
456 if fullpath
== self
._build
_root
:
457 dirnames
.remove(dirname
)
459 relpath
= self
._get
_rel
_path
(fullpath
)
460 self
._dirs
[relpath
] = Directory(fullpath
, relpath
, currentdir
)
461 extensions
= ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu', '.bm')
462 for filename
in filenames
:
463 basename
, extension
= os
.path
.splitext(filename
)
464 if extension
in extensions
:
465 fullpath
= os
.path
.join(dirpath
, filename
)
466 relpath
= self
._get
_rel
_path
(fullpath
)
467 self
._files
[relpath
] = File(fullpath
, relpath
, currentdir
)
468 elif extension
== '.cmakein':
469 extension
= os
.path
.splitext(basename
)[1]
470 if extension
in extensions
:
471 fullpath
= os
.path
.join(dirpath
, basename
)
472 relpath
= self
._get
_rel
_path
(fullpath
)
473 fullpath
= os
.path
.join(dirpath
, filename
)
474 self
._files
[relpath
] = GeneratedFile(fullpath
, relpath
, currentdir
)
476 def _create_module(self
, rootdir
):
477 """Create module for a subdirectory."""
478 name
= 'module_' + rootdir
.get_name()
479 moduleobj
= Module(name
, rootdir
)
480 rootdir
.set_module(moduleobj
)
481 self
._modules
[name
] = moduleobj
483 def scan_files(self
):
484 """Read source files to initialize #include dependencies."""
485 for fileobj
in self
._files
.itervalues():
486 if not fileobj
.is_external():
487 fileobj
.scan_contents(self
)
489 def load_xml(self
, only_files
=False):
490 """Load Doxygen XML information.
492 If only_files is True, XML data is not loaded for code constructs, but
493 only for files, directories, and their potential parents.
495 xmldir
= os
.path
.join(self
._build
_root
, 'doxygen', 'xml')
496 self
._docset
= xml
.DocumentationSet(xmldir
, self
._reporter
)
498 self
._docset
.load_file_details()
500 self
._docset
.load_details()
501 self
._docset
.merge_duplicates()
508 def _load_dirs(self
):
509 """Load Doxygen XML directory information."""
510 rootdirs
= self
._docset
.get_compounds(xml
.Directory
,
511 lambda x
: x
.get_parent() is None)
512 for dirdoc
in rootdirs
:
513 self
._load
_dir
(dirdoc
, None)
515 def _load_dir(self
, dirdoc
, parent
):
516 """Load Doxygen XML directory information for a single directory."""
517 path
= dirdoc
.get_path().rstrip('/')
518 if not os
.path
.isabs(path
):
519 self
._reporter
.xml_assert(dirdoc
.get_xml_path(),
520 "expected absolute path in Doxygen-produced XML file")
522 relpath
= self
._get
_rel
_path
(path
)
523 dirobj
= self
._dirs
.get(relpath
)
525 dirobj
= Directory(path
, relpath
, parent
)
526 self
._dirs
[relpath
] = dirobj
527 dirobj
.set_doc_xml(dirdoc
, self
)
528 self
._docmap
[dirdoc
] = dirobj
529 for subdirdoc
in dirdoc
.get_subdirectories():
530 self
._load
_dir
(subdirdoc
, dirobj
)
532 def _load_modules(self
):
533 """Load Doxygen XML module (group) information."""
534 moduledocs
= self
._docset
.get_compounds(xml
.Group
,
535 lambda x
: x
.get_name().startswith('module_'))
536 for moduledoc
in moduledocs
:
537 moduleobj
= self
._modules
.get(moduledoc
.get_name())
539 self
._reporter
.input_error(
540 "no matching directory for module: {0}".format(moduledoc
))
542 moduleobj
.set_doc_xml(moduledoc
, self
)
543 self
._docmap
[moduledoc
] = moduleobj
545 def _load_files(self
):
546 """Load Doxygen XML file information."""
547 for filedoc
in self
._docset
.get_files():
548 path
= filedoc
.get_path()
549 if not os
.path
.isabs(path
):
550 self
._reporter
.xml_assert(filedoc
.get_xml_path(),
551 "expected absolute path in Doxygen-produced XML file")
553 extension
= os
.path
.splitext(filedoc
.get_path())[1]
554 # We don't care about Markdown files that only produce pages
555 # (and fail the directory check below).
556 if extension
== '.md':
558 dirdoc
= filedoc
.get_directory()
560 self
._reporter
.xml_assert(filedoc
.get_xml_path(),
561 "file is not in any directory in Doxygen")
563 relpath
= self
._get
_rel
_path
(path
)
564 fileobj
= self
._files
.get(relpath
)
566 fileobj
= File(path
, relpath
, self
._docmap
[dirdoc
])
567 self
._files
[relpath
] = fileobj
568 fileobj
.set_doc_xml(filedoc
, self
)
569 self
._docmap
[filedoc
] = fileobj
571 def _load_classes(self
):
572 """Load Doxygen XML class information."""
573 classdocs
= self
._docset
.get_classes()
574 for classdoc
in classdocs
:
575 files
= [self
._docmap
[filedoc
] for filedoc
in classdoc
.get_files()]
576 classobj
= Class(classdoc
, files
)
577 self
._docmap
[classdoc
] = classobj
578 self
._classes
.add(classobj
)
580 def _get_dir(self
, relpath
):
581 """Get directory object for a path relative to source tree root."""
582 return self
._dirs
.get(relpath
)
584 def get_file(self
, path
):
585 """Get file object for a path relative to source tree root."""
586 return self
._files
.get(self
._get
_rel
_path
(path
))
588 def find_include_file(self
, includedpath
):
589 """Find a file object corresponding to an include path."""
590 for testdir
in ('src', 'src/gromacs/legacyheaders', 'src/external/thread_mpi/include'):
591 testpath
= os
.path
.join(testdir
, includedpath
)
592 if testpath
in self
._files
:
593 return self
._files
[testpath
]
595 def set_installed_file_list(self
, installedfiles
):
596 """Set list of installed files."""
597 for path
in installedfiles
:
598 if not os
.path
.isabs(path
):
599 self
._reporter
.input_error(
600 "installed file not specified with absolute path: {0}"
603 relpath
= self
._get
_rel
_path
(path
)
604 if relpath
not in self
._files
:
605 self
._reporter
.input_error(
606 "installed file not in source tree: {0}".format(path
))
608 self
._files
[relpath
].set_installed()
610 def get_object(self
, docobj
):
611 """Get tree object for a Doxygen XML object."""
612 return self
._docmap
.get(docobj
)
615 """Get iterable for all files in the source tree."""
616 return self
._files
.itervalues()
618 def get_modules(self
):
619 """Get iterable for all modules in the source tree."""
620 return self
._modules
.itervalues()
622 def get_classes(self
):
623 """Get iterable for all classes in the source tree."""
626 def get_members(self
):
627 """Get iterable for all members (in Doxygen terms) in the source tree."""
628 # TODO: Add wrappers to solve some issues.
629 return self
._docset
.get_members()