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