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