Move endian, float format and XDR settings to cmakedefine01
[alexxy/gromacs.git] / docs / doxygen / gmxtree.py
1 #!/usr/bin/python
2 #
3 # This file is part of the GROMACS molecular simulation package.
4 #
5 # Copyright (c) 2014,2015, 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.
9 #
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.
14 #
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.
19 #
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.
24 #
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.
32 #
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.
35
36 """GROMACS-specific representation for source tree and documentation.
37
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.
45
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.
51 """
52
53 import collections
54 import os
55 import os.path
56 import re
57 import subprocess
58
59 import doxygenxml as xml
60 import reporter
61 # We import DocType directly so that it is exposed from this module as well.
62 from doxygenxml import DocType
63
64 def _get_api_type_for_compound(grouplist):
65     """Helper function to deduce API type from Doxygen group membership."""
66     result = DocType.internal
67     for group in grouplist:
68         if isinstance(group, xml.Group):
69             if group.get_name() == 'group_publicapi':
70                 result = DocType.public
71             elif group.get_name() == 'group_libraryapi':
72                 result = DocType.library
73             # TODO: Check for multiple group membership
74     return result
75
76 class IncludedFile(object):
77
78     """Information about an #include directive in a file."""
79
80     def __init__(self, including_file, lineno, included_file, included_path, is_relative, is_system, line):
81         self._including_file = including_file
82         self._line_number = lineno
83         self._included_file = included_file
84         self._included_path = included_path
85         #self._used_include_path = used_include_path
86         self._is_relative = is_relative
87         self._is_system = is_system
88         self._line = line
89
90     def __str__(self):
91         if self._is_system:
92             return '<{0}>'.format(self._included_path)
93         else:
94             return '"{0}"'.format(self._included_path)
95
96     def is_system(self):
97         return self._is_system
98
99     def is_relative(self):
100         return self._is_relative
101
102     def get_included_path(self):
103         return self._included_path
104
105     def get_including_file(self):
106         return self._including_file
107
108     def get_file(self):
109         return self._included_file
110
111     def get_line_number(self):
112         return self._line_number
113
114     def get_full_line(self):
115         """Return the full source line on which this include appears.
116
117         Trailing newline is included."""
118         return self._line
119
120     def get_reporter_location(self):
121         return reporter.Location(self._including_file.get_abspath(), self._line_number)
122
123 class IncludeBlock(object):
124
125     """Block of consequent #include directives in a file."""
126
127     def __init__(self, first_included_file):
128         self._first_line = first_included_file.get_line_number()
129         self._last_line = self._first_line
130         self._files = []
131         self.add_file(first_included_file)
132
133     def add_file(self, included_file):
134         self._files.append(included_file)
135         self._last_line = included_file.get_line_number()
136
137     def get_includes(self):
138         return self._files
139
140     def get_first_line(self):
141         return self._first_line
142
143     def get_last_line(self):
144         return self._last_line
145
146 class File(object):
147
148     """Source/header file in the GROMACS tree."""
149
150     def __init__(self, abspath, relpath, directory):
151         """Initialize a file representation with basic information."""
152         self._abspath = abspath
153         self._relpath = relpath
154         self._dir = directory
155         self._rawdoc = None
156         self._installed = False
157         extension = os.path.splitext(abspath)[1]
158         self._sourcefile = (extension in ('.c', '.cc', '.cpp', '.cu'))
159         self._apitype = DocType.none
160         self._modules = set()
161         self._includes = []
162         self._include_blocks = []
163         self._main_header = None
164         self._lines = None
165         self._filter = None
166         self._declared_defines = None
167         self._used_define_files = set()
168         directory.add_file(self)
169
170     def set_doc_xml(self, rawdoc, sourcetree):
171         """Assiociate Doxygen documentation entity with the file."""
172         assert self._rawdoc is None
173         assert rawdoc.is_source_file() == self._sourcefile
174         self._rawdoc = rawdoc
175         if self._rawdoc.is_documented():
176             grouplist = self._rawdoc.get_groups()
177             self._apitype = _get_api_type_for_compound(grouplist)
178             for group in grouplist:
179                 module = sourcetree.get_object(group)
180                 if module:
181                     self._modules.add(module)
182
183     def set_installed(self):
184         """Mark the file installed."""
185         self._installed = True
186
187     def set_git_filter_attribute(self, filtername):
188         """Set the git filter attribute associated with the file."""
189         self._filter = filtername
190
191     def set_main_header(self, included_file):
192         """Set the main header file for a source file."""
193         assert self.is_source_file()
194         self._main_header = included_file
195
196     def _process_include(self, lineno, is_system, includedpath, line, sourcetree):
197         """Process #include directive during scan()."""
198         is_relative = False
199         if is_system:
200             fileobj = sourcetree.find_include_file(includedpath)
201         else:
202             fullpath = os.path.join(self._dir.get_abspath(), includedpath)
203             fullpath = os.path.abspath(fullpath)
204             if os.path.exists(fullpath):
205                 is_relative = True
206                 fileobj = sourcetree.get_file(fullpath)
207             else:
208                 fileobj = sourcetree.find_include_file(includedpath)
209         included_file = IncludedFile(self, lineno, fileobj, includedpath,
210             is_relative, is_system, line)
211         self._includes.append(included_file)
212         return included_file
213
214     def scan_contents(self, sourcetree, keep_contents, detect_defines):
215         """Scan the file contents and initialize information based on it."""
216         # TODO: Consider a more robust regex.
217         include_re = r'^\s*#\s*include\s+(?P<quote>["<])(?P<path>[^">]*)[">]'
218         define_re = r'^\s*#.*define(?:01)?\s+(\w*)'
219         current_block = None
220         with open(self._abspath, 'r') as scanfile:
221             contents = scanfile.read()
222         lines = contents.splitlines(True)
223         for lineno, line in enumerate(lines, 1):
224             match = re.match(include_re, line)
225             if match:
226                 is_system = (match.group('quote') == '<')
227                 includedpath = match.group('path')
228                 included_file = self._process_include(lineno, is_system,
229                         includedpath, line, sourcetree)
230                 if current_block is None:
231                     current_block = IncludeBlock(included_file)
232                     self._include_blocks.append(current_block)
233                 else:
234                     current_block.add_file(included_file)
235             elif line and not line.isspace():
236                 current_block = None
237         if detect_defines:
238             self._declared_defines = []
239             for line in lines:
240                 match = re.match(define_re, line)
241                 if match:
242                     self._declared_defines.append(match.group(1))
243         if keep_contents:
244             self._lines = lines
245
246     def add_used_defines(self, define_file, defines):
247         """Add defines used in this file.
248
249         Used internally by find_define_file_uses()."""
250         self._used_define_files.add(define_file)
251
252     def get_reporter_location(self):
253         return reporter.Location(self._abspath, None)
254
255     def is_installed(self):
256         return self._installed
257
258     def is_external(self):
259         return self._dir.is_external()
260
261     def is_source_file(self):
262         return self._sourcefile
263
264     def is_test_file(self):
265         return self._dir.is_test_directory()
266
267     def should_includes_be_sorted(self):
268         """Return whether the include directives in the file should be sorted."""
269         return self._filter in ('includesort', 'uncrustify')
270
271     def is_documented(self):
272         return self._rawdoc and self._rawdoc.is_documented()
273
274     def has_brief_description(self):
275         return self._rawdoc and self._rawdoc.has_brief_description()
276
277     def get_abspath(self):
278         return self._abspath
279
280     def get_relpath(self):
281         return self._relpath
282
283     def get_name(self):
284         return os.path.basename(self._abspath)
285
286     def get_directory(self):
287         return self._dir
288
289     def get_doc_type(self):
290         if not self._rawdoc:
291             return DocType.none
292         return self._rawdoc.get_visibility()
293
294     def get_api_type(self):
295         return self._apitype
296
297     def api_type_is_reliable(self):
298         if self._apitype in (DocType.internal, DocType.library):
299             return True
300         module = self.get_module()
301         return module and module.is_documented()
302
303     def is_public(self):
304         if self.api_type_is_reliable():
305             return self.get_api_type() == DocType.public
306         return self.get_api_type() == DocType.public or self.is_installed()
307
308     def is_module_internal(self):
309         if self.is_source_file():
310             return True
311         return not self.is_installed() and self.get_api_type() <= DocType.internal
312
313     def get_expected_module(self):
314         return self._dir.get_module()
315
316     def get_doc_modules(self):
317         return self._modules
318
319     def get_module(self):
320         module = self.get_expected_module()
321         if not module and len(self._modules) == 1:
322             module = list(self._modules)[0]
323         return module
324
325     def get_includes(self):
326         return self._includes
327
328     def get_include_blocks(self):
329         return self._include_blocks
330
331     def _get_included_files_recurse(self, result):
332         for include in self._includes:
333             included_file = include.get_file()
334             if included_file is not None and not included_file in result:
335                 result.add(included_file)
336                 included_file._get_included_files_recurse(result)
337
338     def get_included_files(self, recursive=False):
339         if recursive:
340             result = set()
341             self._get_included_files_recurse(result)
342             return result
343         return set([x.get_file() for x in self._includes])
344
345     def get_main_header(self):
346         return self._main_header
347
348     def get_contents(self):
349         return self._lines
350
351     def get_declared_defines(self):
352         """Return set of defines declared in this file.
353
354         The information is only populated for selected files."""
355         return self._declared_defines
356
357     def get_used_define_files(self):
358         """Return set of defines from config.h that are used in this file.
359
360         The return value is empty if find_define_file_uses() has not been called,
361         as well as for headers that declare these defines."""
362         return self._used_define_files
363
364 class GeneratedFile(File):
365     def __init__(self, abspath, relpath, directory):
366         File.__init__(self, abspath, relpath, directory)
367         self._generator_source_file = None
368
369     def scan_contents(self, sourcetree, keep_contents, detect_defines):
370         if os.path.exists(self.get_abspath()):
371             File.scan_contents(self, sourcetree, keep_contents, False)
372
373     def set_generator_source(self, sourcefile):
374         self._generator_source_file = sourcefile
375
376     def get_generator_source(self):
377         return self._generator_source_file
378
379     def get_reporter_location(self):
380         if self._generator_source_file:
381             return self._generator_source_file.get_reporter_location()
382         return File.get_reporter_location(self)
383
384     def get_declared_defines(self):
385         if self._generator_source_file:
386             return self._generator_source_file.get_declared_defines()
387         return File.get_declared_defines(self)
388
389 class GeneratorSourceFile(File):
390     pass
391
392 class Directory(object):
393
394     """(Sub)directory in the GROMACS tree."""
395
396     def __init__(self, abspath, relpath, parent):
397         """Initialize a file representation with basic information."""
398         self._abspath = abspath
399         self._relpath = relpath
400         self._name = os.path.basename(abspath)
401         self._parent = parent
402         self._rawdoc = None
403         self._module = None
404         self._is_test_dir = False
405         if parent and parent.is_test_directory() or \
406                 self._name in ('tests', 'legacytests'):
407             self._is_test_dir = True
408         self._is_external = False
409         if parent and parent.is_external() or self._name == 'external':
410             self._is_external = True
411         self._subdirs = set()
412         if parent:
413             parent._subdirs.add(self)
414         self._files = set()
415         self._has_installed_files = None
416
417     def set_doc_xml(self, rawdoc, sourcetree):
418         """Assiociate Doxygen documentation entity with the directory."""
419         assert self._rawdoc is None
420         assert self._abspath == rawdoc.get_path().rstrip('/')
421         self._rawdoc = rawdoc
422
423     def set_module(self, module):
424         assert self._module is None
425         self._module = module
426
427     def add_file(self, fileobj):
428         self._files.add(fileobj)
429
430     def get_name(self):
431         return self._name
432
433     def get_reporter_location(self):
434         return reporter.Location(self._abspath, None)
435
436     def get_abspath(self):
437         return self._abspath
438
439     def get_relpath(self):
440         return self._relpath
441
442     def is_test_directory(self):
443         return self._is_test_dir
444
445     def is_external(self):
446         return self._is_external
447
448     def has_installed_files(self):
449         if self._has_installed_files is None:
450             self._has_installed_files = False
451             for subdir in self._subdirs:
452                 if subdir.has_installed_files():
453                     self._has_installed_files = True
454                     return True
455             for fileobj in self._files:
456                 if fileobj.is_installed():
457                     self._has_installed_files = True
458                     return True
459         return self._has_installed_files
460
461     def get_module(self):
462         if self._module:
463             return self._module
464         if self._parent:
465             return self._parent.get_module()
466         return None
467
468     def get_subdirectories(self):
469         return self._subdirs
470
471     def get_files(self):
472         for subdir in self._subdirs:
473             for fileobj in subdir.get_files():
474                 yield fileobj
475         for fileobj in self._files:
476             yield fileobj
477
478     def contains(self, fileobj):
479         """Check whether file is within the directory or its subdirectories."""
480         dirobj = fileobj.get_directory()
481         while dirobj:
482             if dirobj == self:
483                 return True
484             dirobj = dirobj._parent
485         return False
486
487 class ModuleDependency(object):
488
489     """Dependency between modules."""
490
491     def __init__(self, othermodule):
492         """Initialize empty dependency object with given module as dependency."""
493         self._othermodule = othermodule
494         self._includedfiles = []
495         self._cyclesuppression = None
496         self._is_test_only_dependency = True
497
498     def add_included_file(self, includedfile):
499         """Add IncludedFile that is part of this dependency."""
500         assert includedfile.get_file().get_module() == self._othermodule
501         if not includedfile.get_including_file().is_test_file():
502             self._is_test_only_dependency = False
503         self._includedfiles.append(includedfile)
504
505     def set_cycle_suppression(self):
506         """Set suppression on cycles containing this dependency."""
507         self._cyclesuppression = True
508
509     def is_cycle_suppressed(self):
510         """Return whether cycles containing this dependency are suppressed."""
511         return self._cyclesuppression is not None
512
513     def is_test_only_dependency(self):
514         """Return whether this dependency is only from test code."""
515         return self._is_test_only_dependency
516
517     def get_other_module(self):
518         """Get module that this dependency is to."""
519         return self._othermodule
520
521     def get_included_files(self):
522         """Get IncludedFile objects for the individual include dependencies."""
523         return self._includedfiles
524
525 class Module(object):
526
527     """Code module in the GROMACS source tree.
528
529     Modules are specific subdirectories that host a more or less coherent
530     set of routines.  Simplified, every subdirectory under src/gromacs/ is
531     a different module.  This object provides that abstraction and also links
532     the subdirectory to the module documentation (documented as a group in
533     Doxygen) if that exists.
534     """
535
536     def __init__(self, name, rootdir):
537         self._name = name
538         self._rawdoc = None
539         self._rootdir = rootdir
540         self._group = None
541         self._dependencies = dict()
542
543     def set_doc_xml(self, rawdoc, sourcetree):
544         """Assiociate Doxygen documentation entity with the module."""
545         assert self._rawdoc is None
546         self._rawdoc = rawdoc
547         if self._rawdoc.is_documented():
548             groups = list(self._rawdoc.get_groups())
549             if len(groups) == 1:
550                 groupname = groups[0].get_name()
551                 if groupname.startswith('group_'):
552                     self._group = groupname[6:]
553
554     def add_dependency(self, othermodule, includedfile):
555         """Add #include dependency from a file in this module."""
556         assert includedfile.get_file().get_module() == othermodule
557         if othermodule not in self._dependencies:
558             self._dependencies[othermodule] = ModuleDependency(othermodule)
559         self._dependencies[othermodule].add_included_file(includedfile)
560
561     def is_documented(self):
562         return self._rawdoc is not None
563
564     def get_name(self):
565         return self._name
566
567     def get_root_dir(self):
568         return self._rootdir
569
570     def get_files(self):
571         # TODO: Include public API convenience headers?
572         return self._rootdir.get_files()
573
574     def get_group(self):
575         return self._group
576
577     def get_dependencies(self):
578         return self._dependencies.itervalues()
579
580 class Namespace(object):
581
582     """Namespace in the GROMACS source code."""
583
584     def __init__(self, rawdoc):
585         self._rawdoc = rawdoc
586
587     def is_anonymous(self):
588         return self._rawdoc.is_anonymous()
589
590 class Class(object):
591
592     """Class/struct/union in the GROMACS source code."""
593
594     def __init__(self, rawdoc, files):
595         self._rawdoc = rawdoc
596         self._files = set(files)
597
598     def get_name(self):
599         return self._rawdoc.get_name()
600
601     def get_reporter_location(self):
602         return self._rawdoc.get_reporter_location()
603
604     def get_files(self):
605         return self._files
606
607     def is_documented(self):
608         return self._rawdoc.is_documented()
609
610     def has_brief_description(self):
611         return self._rawdoc.has_brief_description()
612
613     def get_doc_type(self):
614         """Return documentation type (visibility) for the class.
615
616         In addition to the actual code, this encodes GROMACS-specific logic
617         of setting EXTRACT_LOCAL_CLASSES=YES only for the full documentation.
618         Local classes never appear outside the full documentation, no matter
619         what is their visibility.
620         """
621         if not self.is_documented():
622             return DocType.none
623         if self._rawdoc.is_local():
624             return DocType.internal
625         return self._rawdoc.get_visibility()
626
627     def get_file_doc_type(self):
628         return max([fileobj.get_doc_type() for fileobj in self._files])
629
630     def is_in_installed_file(self):
631         return any([fileobj.is_installed() for fileobj in self._files])
632
633 class Member(object):
634
635     """Member (in Doxygen terminology) in the GROMACS source tree.
636
637     Currently, modeling is limited to the minimal set of properties that the
638     checker uses.
639     """
640
641     def __init__(self, rawdoc, namespace):
642         self._rawdoc = rawdoc
643         self._namespace = namespace
644
645     def get_name(self):
646         return self._rawdoc.get_name()
647
648     def get_reporter_location(self):
649         return self._rawdoc.get_reporter_location()
650
651     def is_documented(self):
652         return self._rawdoc.is_documented()
653
654     def has_brief_description(self):
655         return self._rawdoc.has_brief_description()
656
657     def has_inbody_description(self):
658         return self._rawdoc.has_inbody_description()
659
660     def is_visible(self):
661         """Return whether the member is visible in Doxygen documentation.
662
663         Doxygen ignores members whose parent compounds are not documented.
664         However, when EXTRACT_ANON_NPACES=ON (which is set for our full
665         documentation), members of anonymous namespaces are extracted even if
666         the namespace is the only parent and is not documented.
667         """
668         if self._namespace and self._namespace.is_anonymous():
669             return True
670         return self._rawdoc.get_inherited_visibility() != DocType.none
671
672
673 class GromacsTree(object):
674
675     """Root object for navigating the GROMACS source tree.
676
677     On initialization, the list of files and directories is initialized by
678     walking the source tree, and modules are created for top-level
679     subdirectories.  At this point, only information that is accessible from
680     file names and paths only is available.
681
682     load_git_attributes() can be called to load attribute information from
683     .gitattributes for all the files.
684
685     load_installed_file_list() can be called to load the list of installed
686     files from the build tree (generated by CMake).
687
688     scan_files() can be called to read all the files and initialize #include
689     dependencies between the files based on the information.  This is done like
690     this instead of relying on Doxygen-extracted include files to make the
691     dependency graph independent from preprocessor macro definitions
692     (Doxygen only sees those #includes that the preprocessor sees, which
693     depends on what #defines it has seen).
694
695     find_define_file_uses() can be called to find all uses of defines
696     declared in config.h and some other macro headers. In the current
697     implementation, scan_files() must have been called earlier.
698
699     load_xml() can be called to load information from Doxygen XML data in
700     the build tree (the Doxygen XML data must have been built separately).
701     """
702
703     def __init__(self, source_root, build_root, reporter):
704         """Initialize the tree object by walking the source tree."""
705         self._source_root = os.path.abspath(source_root)
706         self._build_root = os.path.abspath(build_root)
707         self._reporter = reporter
708         self._docset = None
709         self._docmap = dict()
710         self._dirs = dict()
711         self._files = dict()
712         self._modules = dict()
713         self._classes = set()
714         self._namespaces = set()
715         self._members = set()
716         self._walk_dir(os.path.join(self._source_root, 'src'))
717         for fileobj in self.get_files():
718             if fileobj and fileobj.is_source_file() and not fileobj.is_external():
719                 (basedir, name) = os.path.split(fileobj.get_abspath())
720                 (basename, ext) = os.path.splitext(name)
721                 header = self.get_file(os.path.join(basedir, basename + '.h'))
722                 if not header and ext == '.cu':
723                     header = self.get_file(os.path.join(basedir, basename + '.cuh'))
724                 if not header and fileobj.is_test_file():
725                     basedir = os.path.dirname(basedir)
726                     header = self.get_file(os.path.join(basedir, basename + '.h'))
727                     if not header:
728                         # Somewhat of a hack; currently, the tests for
729                         # analysisdata/modules/ and trajectoryanalysis/modules/
730                         # is at the top-level tests directory.
731                         # TODO: It could be clearer to split the tests so that
732                         # there would be a separate modules/tests/.
733                         header = self.get_file(os.path.join(basedir, 'modules', basename + '.h'))
734                     if not header and basename.endswith('_tests'):
735                         header = self.get_file(os.path.join(basedir, basename[:-6] + '.h'))
736                 if not header and fileobj.get_relpath().startswith('src/gromacs'):
737                     header = self._files.get(os.path.join('src/gromacs/legacyheaders', basename + '.h'))
738                 if header:
739                     fileobj.set_main_header(header)
740         rootdir = self._get_dir(os.path.join('src', 'gromacs'))
741         for subdir in rootdir.get_subdirectories():
742             self._create_module(subdir)
743         rootdir = self._get_dir(os.path.join('src', 'testutils'))
744         self._create_module(rootdir)
745
746     def _get_rel_path(self, path):
747         assert os.path.isabs(path)
748         if path.startswith(self._build_root):
749             return os.path.relpath(path, self._build_root)
750         if path.startswith(self._source_root):
751             return os.path.relpath(path, self._source_root)
752         raise ValueError("path not under build nor source tree: {0}".format(path))
753
754     def _walk_dir(self, rootpath):
755         """Construct representation of the source tree by walking the file system."""
756         assert os.path.isabs(rootpath)
757         assert rootpath not in self._dirs
758         relpath = self._get_rel_path(rootpath)
759         self._dirs[relpath] = Directory(rootpath, relpath, None)
760         for dirpath, dirnames, filenames in os.walk(rootpath):
761             if 'contrib' in dirnames:
762                 dirnames.remove('contrib')
763             if 'refdata' in dirnames:
764                 dirnames.remove('refdata')
765             currentdir = self._dirs[self._get_rel_path(dirpath)]
766             # Loop through a copy so that we can modify dirnames.
767             for dirname in list(dirnames):
768                 fullpath = os.path.join(dirpath, dirname)
769                 if fullpath == self._build_root:
770                     dirnames.remove(dirname)
771                     continue
772                 relpath = self._get_rel_path(fullpath)
773                 self._dirs[relpath] = Directory(fullpath, relpath, currentdir)
774             extensions = ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu', '.bm')
775             for filename in filenames:
776                 basename, extension = os.path.splitext(filename)
777                 if extension in extensions:
778                     fullpath = os.path.join(dirpath, filename)
779                     relpath = self._get_rel_path(fullpath)
780                     self._files[relpath] = File(fullpath, relpath, currentdir)
781                 elif extension == '.cmakein':
782                     extension = os.path.splitext(basename)[1]
783                     if extension in extensions:
784                         fullpath = os.path.join(dirpath, filename)
785                         relpath = self._get_rel_path(fullpath)
786                         sourcefile = GeneratorSourceFile(fullpath, relpath, currentdir)
787                         self._files[relpath] = sourcefile
788                         fullpath = os.path.join(dirpath, basename)
789                         relpath = self._get_rel_path(fullpath)
790                         fullpath = os.path.join(self._build_root, relpath)
791                         generatedfile = GeneratedFile(fullpath, relpath, currentdir)
792                         self._files[relpath] = generatedfile
793                         generatedfile.set_generator_source(sourcefile)
794                 elif extension in ('.l', '.y', '.pre'):
795                     fullpath = os.path.join(dirpath, filename)
796                     relpath = self._get_rel_path(fullpath)
797                     self._files[relpath] = GeneratorSourceFile(fullpath, relpath, currentdir)
798
799     def _create_module(self, rootdir):
800         """Create module for a subdirectory."""
801         name = 'module_' + rootdir.get_name()
802         moduleobj = Module(name, rootdir)
803         rootdir.set_module(moduleobj)
804         self._modules[name] = moduleobj
805
806     def scan_files(self, only_files=None, keep_contents=False):
807         """Read source files to initialize #include dependencies."""
808         if only_files:
809             filelist = only_files
810         else:
811             filelist = self._files.itervalues()
812         define_files = list(self.get_checked_define_files())
813         for define_file in list(define_files):
814             if isinstance(define_file, GeneratedFile) and \
815                     define_file.get_generator_source() is not None:
816                 define_files.append(define_file.get_generator_source())
817         for fileobj in filelist:
818             if not fileobj.is_external():
819                 detect_defines = fileobj in define_files
820                 fileobj.scan_contents(self, keep_contents, detect_defines)
821                 module = fileobj.get_module()
822                 if module:
823                     for includedfile in fileobj.get_includes():
824                         otherfile = includedfile.get_file()
825                         if otherfile:
826                             othermodule = otherfile.get_module()
827                             if othermodule and othermodule != module:
828                                 module.add_dependency(othermodule, includedfile)
829
830     def load_xml(self, only_files=None):
831         """Load Doxygen XML information.
832
833         If only_files is True, XML data is not loaded for code constructs, but
834         only for files, directories, and their potential parents.
835         """
836         xmldir = os.path.join(self._build_root, 'docs', 'html', 'doxygen', 'xml')
837         self._docset = xml.DocumentationSet(xmldir, self._reporter)
838         if only_files:
839             if isinstance(only_files, collections.Iterable):
840                 filelist = [x.get_abspath() for x in only_files]
841                 self._docset.load_file_details(filelist)
842             else:
843                 self._docset.load_file_details()
844         else:
845             self._docset.load_details()
846             self._docset.merge_duplicates()
847         self._load_dirs()
848         self._load_modules()
849         self._load_files()
850         if not only_files:
851             self._load_namespaces()
852             self._load_classes()
853             self._load_members()
854
855     def _load_dirs(self):
856         """Load Doxygen XML directory information."""
857         rootdirs = self._docset.get_compounds(xml.Directory,
858                 lambda x: x.get_parent() is None)
859         for dirdoc in rootdirs:
860             self._load_dir(dirdoc, None)
861
862     def _load_dir(self, dirdoc, parent):
863         """Load Doxygen XML directory information for a single directory."""
864         path = dirdoc.get_path().rstrip('/')
865         if not os.path.isabs(path):
866             self._reporter.xml_assert(dirdoc.get_xml_path(),
867                     "expected absolute path in Doxygen-produced XML file")
868             return
869         relpath = self._get_rel_path(path)
870         dirobj = self._dirs.get(relpath)
871         if not dirobj:
872             dirobj = Directory(path, relpath, parent)
873             self._dirs[relpath] = dirobj
874         dirobj.set_doc_xml(dirdoc, self)
875         self._docmap[dirdoc] = dirobj
876         for subdirdoc in dirdoc.get_subdirectories():
877             self._load_dir(subdirdoc, dirobj)
878
879     def _load_modules(self):
880         """Load Doxygen XML module (group) information."""
881         moduledocs = self._docset.get_compounds(xml.Group,
882                 lambda x: x.get_name().startswith('module_'))
883         for moduledoc in moduledocs:
884             moduleobj = self._modules.get(moduledoc.get_name())
885             if not moduleobj:
886                 self._reporter.input_error(
887                         "no matching directory for module: {0}".format(moduledoc))
888                 continue
889             moduleobj.set_doc_xml(moduledoc, self)
890             self._docmap[moduledoc] = moduleobj
891
892     def _load_files(self):
893         """Load Doxygen XML file information."""
894         for filedoc in self._docset.get_files():
895             path = filedoc.get_path()
896             if not path:
897                 # In case of only partially loaded file information,
898                 # the path information is not set for unloaded files.
899                 continue
900             if not os.path.isabs(path):
901                 self._reporter.xml_assert(filedoc.get_xml_path(),
902                         "expected absolute path in Doxygen-produced XML file")
903                 continue
904             extension = os.path.splitext(path)[1]
905             # We don't care about Markdown files that only produce pages
906             # (and fail the directory check below).
907             if extension == '.md':
908                 continue
909             dirdoc = filedoc.get_directory()
910             if not dirdoc:
911                 self._reporter.xml_assert(filedoc.get_xml_path(),
912                         "file is not in any directory in Doxygen")
913                 continue
914             relpath = self._get_rel_path(path)
915             fileobj = self._files.get(relpath)
916             if not fileobj:
917                 fileobj = File(path, relpath, self._docmap[dirdoc])
918                 self._files[relpath] = fileobj
919             fileobj.set_doc_xml(filedoc, self)
920             self._docmap[filedoc] = fileobj
921
922     def _load_namespaces(self):
923         """Load Doxygen XML namespace information."""
924         nsdocs = self._docset.get_namespaces()
925         for nsdoc in nsdocs:
926             nsobj = Namespace(nsdoc)
927             self._docmap[nsdoc] = nsobj
928             self._namespaces.add(nsobj)
929
930     def _load_classes(self):
931         """Load Doxygen XML class information."""
932         classdocs = self._docset.get_classes()
933         for classdoc in classdocs:
934             files = [self._docmap[filedoc] for filedoc in classdoc.get_files()]
935             classobj = Class(classdoc, files)
936             self._docmap[classdoc] = classobj
937             self._classes.add(classobj)
938
939     def _load_members(self):
940         """Load Doxygen XML member information."""
941         memberdocs = self._docset.get_members()
942         for memberdoc in memberdocs:
943             nsdoc = memberdoc.get_namespace()
944             nsobj = self.get_object(nsdoc)
945             memberobj = Member(memberdoc, nsobj)
946             self._docmap[memberdoc] = memberobj
947             self._members.add(memberobj)
948
949     def _get_dir(self, relpath):
950         """Get directory object for a path relative to source tree root."""
951         return self._dirs.get(relpath)
952
953     def get_file(self, path):
954         """Get file object for a path relative to source tree root."""
955         return self._files.get(self._get_rel_path(path))
956
957     def find_include_file(self, includedpath):
958         """Find a file object corresponding to an include path."""
959         for testdir in ('src', 'src/external/thread_mpi/include',
960                 'src/external/tng_io/include'):
961             testpath = os.path.join(testdir, includedpath)
962             if testpath in self._files:
963                 return self._files[testpath]
964
965     def load_git_attributes(self):
966         """Load git attribute information for files."""
967         args = ['git', 'check-attr', '--stdin', 'filter']
968         git_check_attr = subprocess.Popen(args, stdin=subprocess.PIPE,
969                 stdout=subprocess.PIPE, cwd=self._source_root)
970         filelist = '\n'.join(map(File.get_relpath, self._files.itervalues()))
971         filters = git_check_attr.communicate(filelist)[0]
972         for fileinfo in filters.splitlines():
973             path, dummy, value = fileinfo.split(': ')
974             fileobj = self._files.get(path)
975             assert fileobj is not None
976             fileobj.set_git_filter_attribute(value)
977
978     def find_define_file_uses(self):
979         """Find files that use defines from config.h."""
980         # Executing git grep is substantially faster than using the define_re
981         # directly on the contents of the file in Python.
982         for define_file in self.get_checked_define_files():
983             excluded_files = set([define_file])
984             excluded_files.update(define_file.get_included_files(recursive=True))
985             all_defines = define_file.get_declared_defines()
986             args = ['git', 'grep', '-zwIF']
987             for define in all_defines:
988                 args.extend(['-e', define])
989             args.extend(['--', '*.cpp', '*.c', '*.cu', '*.h', '*.cuh'])
990             define_re = r'\b(?:' + '|'.join(all_defines)+ r')\b'
991             output = subprocess.check_output(args, cwd=self._source_root)
992             for line in output.splitlines():
993                 (filename, text) = line.split('\0')
994                 fileobj = self._files.get(filename)
995                 if fileobj is not None and fileobj not in excluded_files:
996                     defines = re.findall(define_re, text)
997                     fileobj.add_used_defines(define_file, defines)
998
999     def load_installed_file_list(self):
1000         """Load list of installed files from the build tree."""
1001         listpath = os.path.join(self._build_root, 'src', 'gromacs', 'installed-headers.txt')
1002         with open(listpath, 'r') as installedfp:
1003             for line in installedfp:
1004                 path = line.strip()
1005                 if not os.path.isabs(path):
1006                     self._reporter.input_error(
1007                             "installed file not specified with absolute path: {0}"
1008                             .format(path))
1009                     continue
1010                 relpath = self._get_rel_path(path)
1011                 if relpath not in self._files:
1012                     self._reporter.input_error(
1013                             "installed file not in source tree: {0}".format(path))
1014                     continue
1015                 self._files[relpath].set_installed()
1016
1017     def load_cycle_suppression_list(self, filename):
1018         """Load a list of edges to suppress in cycles.
1019
1020         These edges between modules, if present, will be marked in the
1021         corresponding ModuleDependency objects.
1022         """
1023         with open(filename, 'r') as fp:
1024             for line in fp:
1025                 line = line.strip()
1026                 if not line or line.startswith('#'):
1027                     continue
1028                 modulenames = ['module_' + x.strip() for x in line.split('->')]
1029                 if len(modulenames) != 2:
1030                     self._reporter.input_error(
1031                             "invalid cycle suppression line: {0}".format(line))
1032                     continue
1033                 firstmodule = self._modules.get(modulenames[0])
1034                 secondmodule = self._modules.get(modulenames[1])
1035                 if not firstmodule or not secondmodule:
1036                     self._reporter.input_error(
1037                             "unknown modules mentioned on cycle suppression line: {0}".format(line))
1038                     continue
1039                 for dep in firstmodule.get_dependencies():
1040                     if dep.get_other_module() == secondmodule:
1041                         # TODO: Check that each suppression is actually part of
1042                         # a cycle.
1043                         dep.set_cycle_suppression()
1044
1045     def get_object(self, docobj):
1046         """Get tree object for a Doxygen XML object."""
1047         if docobj is None:
1048             return None
1049         return self._docmap.get(docobj)
1050
1051     def get_files(self):
1052         """Get iterable for all files in the source tree."""
1053         return self._files.itervalues()
1054
1055     def get_modules(self):
1056         """Get iterable for all modules in the source tree."""
1057         return self._modules.itervalues()
1058
1059     def get_classes(self):
1060         """Get iterable for all classes in the source tree."""
1061         return self._classes
1062
1063     def get_members(self):
1064         """Get iterable for all members (in Doxygen terms) in the source tree."""
1065         return self._members
1066
1067     def get_checked_define_files(self):
1068         """Get list of files that contain #define macros whose usage needs to
1069         be checked."""
1070         return (self._files['src/config.h'],
1071                 self._files['src/gromacs/simd/simd.h'],
1072                 self._files['src/gromacs/ewald/pme-simd.h'],
1073                 self._files['src/gromacs/mdlib/nbnxn_simd.h'])