1# Copyright 2014-2017 Insight Software Consortium.
2# Copyright 2004-2009 Roman Yakovenko.
3# Distributed under the Boost Software License, Version 1.0.
4# See http://www.boost.org/LICENSE_1_0.txt
5
6"""
7Defines C++ parser configuration classes.
8
9"""
10
11import os
12import copy
13import platform
14import subprocess
15import warnings
16# In py3, ConfigParser was renamed to the more-standard configparser.
17# But there's a py3 backport that installs "configparser" in py2, and I don't
18# want it because it has annoying deprecation warnings. So try the real py2
19# import first
20# Inspired by https://bitbucket.org/ned/coveragepy/commits/f8e9d62f1412
21try:
22    from ConfigParser import SafeConfigParser as ConfigParser
23except ImportError:
24    from configparser import ConfigParser
25from .. import utils
26
27
28class parser_configuration_t(object):
29
30    """
31    C++ parser configuration holder
32
33    This class serves as a base class for the parameters that can be used
34    to customize the call to a C++ parser.
35
36    This class also allows users to work with relative files paths. In this
37    case files are searched in the following order:
38
39       1. current directory
40       2. working directory
41       3. additional include paths specified by the user
42
43    """
44
45    def __init__(
46            self,
47            working_directory='.',
48            include_paths=None,
49            define_symbols=None,
50            undefine_symbols=None,
51            cflags="",
52            ccflags="",
53            compiler=None,
54            xml_generator=None,
55            keep_xml=False,
56            compiler_path=None,
57            flags=None,
58            castxml_epic_version=None):
59
60        object.__init__(self)
61        self.__working_directory = working_directory
62
63        if not include_paths:
64            include_paths = []
65        self.__include_paths = include_paths
66
67        if not define_symbols:
68            define_symbols = []
69        self.__define_symbols = define_symbols
70
71        if not undefine_symbols:
72            undefine_symbols = []
73        self.__undefine_symbols = undefine_symbols
74
75        self.__cflags = cflags
76
77        self.__ccflags = ccflags
78
79        self.__compiler = compiler
80
81        self.__xml_generator = xml_generator
82
83        self.__castxml_epic_version = castxml_epic_version
84
85        self.__keep_xml = keep_xml
86
87        if flags is None:
88            flags = []
89        self.__flags = flags
90
91        # If no compiler path was set and we are using castxml, set the path
92        self.__compiler_path = create_compiler_path(
93            xml_generator, compiler_path)
94
95    def clone(self):
96        raise NotImplementedError(self.__class__.__name__)
97
98    @property
99    def working_directory(self):
100        return self.__working_directory
101
102    @working_directory.setter
103    def working_directory(self, working_dir):
104        self.__working_directory = working_dir
105
106    @property
107    def include_paths(self):
108        """list of include paths to look for header files"""
109        return self.__include_paths
110
111    @property
112    def define_symbols(self):
113        """list of "define" directives """
114        return self.__define_symbols
115
116    @property
117    def undefine_symbols(self):
118        """list of "undefine" directives """
119        return self.__undefine_symbols
120
121    @property
122    def compiler(self):
123        """get compiler name to simulate"""
124        return self.__compiler
125
126    @compiler.setter
127    def compiler(self, compiler):
128        """set compiler name to simulate"""
129        self.__compiler = compiler
130
131    @property
132    def xml_generator(self):
133        """get xml_generator (gccxml or castxml)"""
134        return self.__xml_generator
135
136    @xml_generator.setter
137    def xml_generator(self, xml_generator):
138        """set xml_generator (gccxml or castxml)"""
139        if "real" in xml_generator:
140            # Support for gccxml.real from newer gccxml package
141            # Can be removed once gccxml support is dropped.
142            xml_generator = "gccxml"
143        self.__xml_generator = xml_generator
144
145    @property
146    def castxml_epic_version(self):
147        """
148        File format version used by castxml.
149        """
150        return self.__castxml_epic_version
151
152    @castxml_epic_version.setter
153    def castxml_epic_version(self, castxml_epic_version):
154        """
155        File format version used by castxml.
156        """
157        self.__castxml_epic_version = castxml_epic_version
158
159    @property
160    def keep_xml(self):
161        """Are xml files kept after errors."""
162        return self.__keep_xml
163
164    @keep_xml.setter
165    def keep_xml(self, keep_xml):
166        """Set if xml files kept after errors."""
167        self.__keep_xml = keep_xml
168
169    @property
170    def flags(self):
171        """Optional flags for pygccxml."""
172        return self.__flags
173
174    @flags.setter
175    def flags(self, flags):
176        """Optional flags for pygccxml."""
177        if flags is None:
178            flags = []
179        self.__flags = flags
180
181    @property
182    def compiler_path(self):
183        """Get the path for the compiler."""
184        return self.__compiler_path
185
186    @compiler_path.setter
187    def compiler_path(self, compiler_path):
188        """Set the path for the compiler."""
189        self.__compiler_path = compiler_path
190
191    @property
192    def cflags(self):
193        """additional flags to pass to compiler"""
194        return self.__cflags
195
196    @cflags.setter
197    def cflags(self, val):
198        self.__cflags = val
199
200    def append_cflags(self, val):
201        self.__cflags = self.__cflags + ' ' + val
202
203    @property
204    def ccflags(self):
205        """
206        additional cross-compatible flags to pass directly
207        to internal simulated compiler.
208        Castxml removes any definitions of its
209        pre-defined macros (e.g. -fopenmp). To propagate these down to the
210        compiler, these flags must also be passed here.
211        See `cc-opt` on castxml's documentation page:
212        https://github.com/CastXML/CastXML/blob/master/doc/manual/castxml.1.rst
213        """
214        return self.__ccflags
215
216    @ccflags.setter
217    def ccflags(self, val):
218        self.__ccflags = val
219
220    def append_ccflags(self, val):
221        self.__ccflags = self.__ccflags + ' ' + val
222
223    def __ensure_dir_exists(self, dir_path, meaning):
224        if os.path.isdir(dir_path):
225            return
226        if os.path.exists(self.working_directory):
227            msg = '%s("%s") does not exist.' % (meaning, dir_path)
228            if meaning == 'include directory':
229                # Warn instead of failing.
230                warnings.warn(msg, RuntimeWarning)
231            else:
232                raise RuntimeError(msg)
233        else:
234            raise RuntimeError(
235                '%s("%s") should be "directory", not a file.' %
236                (meaning, dir_path))
237
238    def raise_on_wrong_settings(self):
239        """
240        Validates the configuration settings and raises RuntimeError on error
241        """
242        self.__ensure_dir_exists(self.working_directory, 'working directory')
243        for idir in self.include_paths:
244            self.__ensure_dir_exists(idir, 'include directory')
245        if self.__xml_generator not in ["castxml", "gccxml"]:
246            msg = ('xml_generator("%s") should either be ' +
247                   '"castxml" or "gccxml".') % self.xml_generator
248            raise RuntimeError(msg)
249
250
251class xml_generator_configuration_t(parser_configuration_t):
252    """
253    Configuration object to collect parameters for invoking gccxml or castxml.
254
255    This class serves as a container for the parameters that can be used
256    to customize the call to gccxml or castxml.
257
258    """
259
260    def __init__(
261            self,
262            gccxml_path='',
263            xml_generator_path='',
264            working_directory='.',
265            include_paths=None,
266            define_symbols=None,
267            undefine_symbols=None,
268            start_with_declarations=None,
269            ignore_gccxml_output=False,
270            cflags="",
271            ccflags="",
272            compiler=None,
273            xml_generator=None,
274            keep_xml=False,
275            compiler_path=None,
276            flags=None,
277            castxml_epic_version=None):
278
279        parser_configuration_t.__init__(
280            self,
281            working_directory=working_directory,
282            include_paths=include_paths,
283            define_symbols=define_symbols,
284            undefine_symbols=undefine_symbols,
285            cflags=cflags,
286            ccflags=ccflags,
287            compiler=compiler,
288            xml_generator=xml_generator,
289            keep_xml=keep_xml,
290            compiler_path=compiler_path,
291            flags=flags,
292            castxml_epic_version=castxml_epic_version)
293
294        if gccxml_path != '':
295            self.__gccxml_path = gccxml_path
296        self.__xml_generator_path = xml_generator_path
297
298        if not start_with_declarations:
299            start_with_declarations = []
300        self.__start_with_declarations = start_with_declarations
301
302        self.__ignore_gccxml_output = ignore_gccxml_output
303
304        self.__xml_generator_from_xml_file = None
305
306    def clone(self):
307        return copy.deepcopy(self)
308
309    @property
310    def xml_generator_path(self):
311        """
312        XML generator binary location
313
314        """
315
316        return self.__xml_generator_path
317
318    @xml_generator_path.setter
319    def xml_generator_path(self, new_path):
320        self.__xml_generator_path = new_path
321
322    @property
323    def xml_generator_from_xml_file(self):
324        """
325        Configuration object containing information about the xml generator
326        read from the xml file.
327
328        Returns:
329            utils.xml_generators: configuration object
330        """
331        return self.__xml_generator_from_xml_file
332
333    @xml_generator_from_xml_file.setter
334    def xml_generator_from_xml_file(self, xml_generator_from_xml_file):
335        self.__xml_generator_from_xml_file = xml_generator_from_xml_file
336
337    @property
338    def start_with_declarations(self):
339        """list of declarations gccxml should start with, when it dumps
340        declaration tree"""
341        return self.__start_with_declarations
342
343    @property
344    def ignore_gccxml_output(self):
345        """set this property to True, if you want pygccxml to ignore any
346            error warning that comes from gccxml"""
347        return self.__ignore_gccxml_output
348
349    @ignore_gccxml_output.setter
350    def ignore_gccxml_output(self, val=True):
351        self.__ignore_gccxml_output = val
352
353    def raise_on_wrong_settings(self):
354        super(xml_generator_configuration_t, self).raise_on_wrong_settings()
355        if self.xml_generator_path is None or \
356                not os.path.isfile(self.xml_generator_path):
357            msg = (
358                'xml_generator_path("%s") should be set and exist.') \
359                % self.xml_generator_path
360            raise RuntimeError(msg)
361
362
363def load_xml_generator_configuration(configuration, **defaults):
364    """
365    Loads CastXML or GCC-XML configuration.
366
367    Args:
368         configuration (string|configparser.ConfigParser): can be
369             a string (file path to a configuration file) or
370             instance of :class:`configparser.ConfigParser`.
371         defaults: can be used to override single configuration values.
372
373    Returns:
374        :class:`.xml_generator_configuration_t`: a configuration object
375
376
377    The file passed needs to be in a format that can be parsed by
378    :class:`configparser.ConfigParser`.
379
380    An example configuration file skeleton can be found
381    `here <https://github.com/gccxml/pygccxml/blob/develop/
382    unittests/xml_generator.cfg>`_.
383
384    """
385    parser = configuration
386    if utils.is_str(configuration):
387        parser = ConfigParser()
388        parser.read(configuration)
389
390    # Create a new empty configuration
391    cfg = xml_generator_configuration_t()
392
393    values = defaults
394    if not values:
395        values = {}
396
397    if parser.has_section('xml_generator'):
398        for name, value in parser.items('xml_generator'):
399            if value.strip():
400                values[name] = value
401
402    for name, value in values.items():
403        if isinstance(value, str):
404            value = value.strip()
405        if name == 'gccxml_path':
406            cfg.gccxml_path = value
407        if name == 'xml_generator_path':
408            cfg.xml_generator_path = value
409        elif name == 'working_directory':
410            cfg.working_directory = value
411        elif name == 'include_paths':
412            for p in value.split(';'):
413                p = p.strip()
414                if p:
415                    cfg.include_paths.append(os.path.normpath(p))
416        elif name == 'compiler':
417            cfg.compiler = value
418        elif name == 'xml_generator':
419            cfg.xml_generator = value
420        elif name == 'castxml_epic_version':
421            cfg.castxml_epic_version = int(value)
422        elif name == 'keep_xml':
423            cfg.keep_xml = value
424        elif name == 'cflags':
425            cfg.cflags = value
426        elif name == 'ccflags':
427            cfg.ccflags = value
428        elif name == 'flags':
429            cfg.flags = value
430        elif name == 'compiler_path':
431            cfg.compiler_path = value
432        else:
433            print('\n%s entry was ignored' % name)
434
435    # If no compiler path was set and we are using castxml, set the path
436    # Here we overwrite the default configuration done in the cfg because
437    # the xml_generator was set through the setter after the creation of a new
438    # emppty configuration object.
439    cfg.compiler_path = create_compiler_path(
440        cfg.xml_generator, cfg.compiler_path)
441
442    return cfg
443
444
445def create_compiler_path(xml_generator, compiler_path):
446    """
447    Try to guess a path for the compiler.
448
449    If you want to use a specific compiler, please provide the compiler
450    path manually, as the guess may not be what you are expecting.
451    Providing the path can be done by passing it as an argument (compiler_path)
452    to the xml_generator_configuration_t() or by defining it in your pygccxml
453    configuration file.
454
455    """
456
457    if xml_generator == 'castxml' and compiler_path is None:
458        if platform.system() == 'Windows':
459            # Look for msvc
460            compiler_path = __get_first_compiler_in_path('where', 'cl')
461            # No msvc found; look for mingw
462            if compiler_path == '':
463                compiler_path = __get_first_compiler_in_path('where', 'mingw')
464        else:
465            # OS X or Linux
466            # Look for clang first, then gcc
467            compiler_path = __get_first_compiler_in_path('which', 'clang++')
468            # No clang found; use gcc
469            if compiler_path == '':
470                compiler_path = __get_first_compiler_in_path('which', 'c++')
471
472        if compiler_path == "":
473            compiler_path = None
474
475    return compiler_path
476
477
478def __get_first_compiler_in_path(command, compiler_name):
479    p = subprocess.Popen(
480        [command, compiler_name],
481        stdout=subprocess.PIPE,
482        stderr=subprocess.PIPE)
483    path = p.stdout.read().decode("utf-8").rstrip().split("\r\n")[0].rstrip()
484    p.wait()
485    p.stdout.close()
486    p.stderr.close()
487    return path
488
489
490if __name__ == '__main__':
491    print(load_xml_generator_configuration('xml_generator.cfg').__dict__)
492