File: Synopsis/Parsers/Cpp/Emulator.py
  1#
  2# Copyright (C) 2005 Stefan Seefeld
  3# All rights reserved.
  4# Licensed to the public under the terms of the GNU LGPL (>= 2),
  5# see the file COPYING for details.
  6#
  7
  8__docformat__ = 'reStructuredText'
  9
 10import sys, os, os.path, re, string, stat, tempfile
 11from Synopsis.config import version
 12
 13class TempFile:
 14    # Use tempfile.NamedTemporaryFile once we can rely on Python 2.4
 15    def __init__(self, suffix):
 16
 17        self.name = tempfile.mktemp(suffix)
 18        self.file = open(self.name, 'w')
 19        self.file.close()
 20
 21
 22    def __del__(self):
 23
 24        os.unlink(self.name)
 25
 26
 27def find_ms_compiler_info():
 28    """
 29    Try to find a (C++) MSVC compiler.
 30    Return tuple of include path list and macro dictionary."""
 31
 32    vc6 = ('SOFTWARE\\Microsoft\\DevStudio\\6.0\\Products\\Microsoft Visual C++', 'ProductDir')
 33    vc7 = ('SOFTWARE\\Microsoft\\VisualStudio\\7.0', 'InstallDir')
 34    vc71 = ('SOFTWARE\\Microsoft\\VisualStudio\\7.1', 'InstallDir')
 35    vc8 = ('SOFTWARE\\Microsoft\\VisualStudio\\8.0', 'InstallDir')
 36    vc9e = ('SOFTWARE\\Microsoft\\VCExpress\\9.0\\Setup\\VC', 'ProductDir')
 37
 38    vc6_macros =  [('__uuidof(x)', 'IID()'),
 39                   ('__int64', 'long long'),
 40                   ('_MSC_VER', '1200'),
 41                   ('_MSC_EXTENSIONS', ''),
 42                   ('_WIN32', ''),
 43                   ('_M_IX86', ''),
 44                   ('_WCHAR_T_DEFINED', ''),
 45                   ('_INTEGRAL_MAX_BITS', '64'),
 46                   ('PASCAL', ''),
 47                   ('RPC_ENTRY', ''),
 48                   ('SHSTDAPI', 'HRESULT'),
 49                   ('SHSTDAPI_(x)', 'x')]
 50    vc6_paths = ['Include']
 51
 52    vc7_macros = [('__forceinline', '__inline'),
 53                  ('__uuidof(x)', 'IID()'),
 54                  ('__w64', ''),
 55                  ('__int64', 'long long'),
 56                  ('_MSC_VER', '1300'),
 57                  ('_MSC_EXTENSIONS', ''),
 58                  ('_WIN32', ''),
 59                  ('_M_IX86', ''),
 60                  ('_WCHAR_T_DEFINED', ''),
 61                  ('_INTEGRAL_MAX_BITS', '64'),
 62                  ('PASCAL', ''),
 63                  ('RPC_ENTRY', ''),
 64                  ('SHSTDAPI', 'HRESULT'),
 65                  ('SHSTDAPI_(x)', 'x')]
 66    vc7_paths = ['..\\..\\Vc7\\Include',
 67                 '..\\..\\Vc7\\PlatformSDK\\Include']
 68
 69    vc71_macros = [('__forceinline', '__inline'),
 70                   ('__uuidof(x)', 'IID()'),
 71                   ('__w64', ''),
 72                   ('__int64', 'long long'),
 73                   ('_MSC_VER', '1310'),
 74                   ('_MSC_EXTENSIONS', ''),
 75                   ('_WIN32', ''),
 76                   ('_M_IX86', ''),
 77                   ('_WCHAR_T_DEFINED', ''),
 78                   ('_INTEGRAL_MAX_BITS', '64'),
 79                   ('PASCAL', ''),
 80                   ('RPC_ENTRY', ''),
 81                   ('SHSTDAPI', 'HRESULT'),
 82                   ('SHSTDAPI_(x)', 'x')]
 83    vc71_paths = ['..\\..\\Vc7\\Include',
 84                  '..\\..\\Vc7\\PlatformSDK\\Include']
 85
 86    vc8_macros = [('__cplusplus', '1'),
 87                  ('__forceinline', '__inline'),
 88                  ('__uuidof(x)', 'IID()'),
 89                  ('__w64', ''),
 90                  ('__int8', 'char'),
 91                  ('__int16', 'short'),
 92                  ('__int32', 'int'),
 93                  ('__int64', 'long long'),
 94                  ('__ptr64', ''),
 95                  ('_MSC_VER', '1400'),
 96                  ('_MSC_EXTENSIONS', ''),
 97                  ('_WIN32', ''),
 98                  ('_M_IX86', ''),
 99                  ('_WCHAR_T_DEFINED', ''),
100                  ('_INTEGRAL_MAX_BITS', '64'),
101                  ('PASCAL', ''),
102                  ('RPC_ENTRY', ''),
103                  ('SHSTDAPI', 'HRESULT'),
104                  ('SHSTDAPI_(x)', 'x')]
105    vc8_paths = ['..\\..\\Vc\\Include',
106                 '..\\..\\Vc\\PlatformSDK\\Include']
107
108    vc9e_macros = [('__cplusplus', '1'),
109                  ('__forceinline', '__inline'),
110                  ('__uuidof(x)', 'IID()'),
111                  ('__w64', ''),
112                  ('__int8', 'char'),
113                  ('__int16', 'short'),
114                  ('__int32', 'int'),
115                  ('__int64', 'long long'),
116                  ('__ptr64', ''),
117                  ('_MSC_VER', '1400'),
118                  ('_MSC_EXTENSIONS', ''),
119                  ('_WIN32', ''),
120                  ('_M_IX86', ''),
121                  ('_WCHAR_T_DEFINED', ''),
122                  ('_INTEGRAL_MAX_BITS', '64'),
123                  ('PASCAL', ''),
124                  ('RPC_ENTRY', ''),
125                  ('SHSTDAPI', 'HRESULT'),
126                  ('SHSTDAPI_(x)', 'x')]
127    vc9e_paths = ['..\\..\\Vc\\Include',
128                 '..\\..\\Vc\\PlatformSDK\\Include']
129
130    compilers = [(vc9e, vc9e_macros, vc9e_paths),
131                 (vc8, vc8_macros, vc8_paths),
132                 (vc71, vc71_macros, vc71_paths),
133                 (vc7, vc7_macros, vc7_paths),
134                 (vc6, vc6_macros, vc6_paths)]
135
136    found, paths, macros = False, [], []
137
138    import _winreg
139    for c in compilers:
140        try:
141            key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, c[0][0])
142            path, type = _winreg.QueryValueEx(key, c[0][1])
143     found = True
144            paths.extend([os.path.join(str(path), p) for p in c[2]])
145            macros.extend(c[1])
146            break
147        except:
148            continue
149
150    return found, paths, macros
151
152def find_gcc_compiler_info(language, compiler, flags):
153    """
154    Try to find a GCC-based C or C++ compiler.
155    Return tuple of include path list and macro dictionary."""
156
157    found, paths, macros = False, [], []
158    flags = ' '.join(flags)
159    temp = TempFile(language == 'C++' and '.cc' or '.c')
160    # The output below assumes the en_US locale, so make sure we use that.
161    command = 'LANG=en_US %s %s -E -v -dD %s'%(compiler, flags, temp.name)
162    try:
163        import subprocess # available only since Python 2.4
164        p = subprocess.Popen(command, shell=True,
165                             stdout=subprocess.PIPE,
166                             stderr=subprocess.PIPE, close_fds=True)
167        stdout, stderr = p.communicate()
168        out, err = stdout.split('\n'), stderr.split('\n')
169    except:
170        stdin, stdout, stderr = os.popen3(command) # for Python < 2.4
171        out, err = stdout.readlines(), stderr.readlines()
172        stdin.close()
173
174    state = 0
175    for line in err:
176        line = line.rstrip()
177        if state == 0:
178            if line[:11] == 'gcc version': state = 1
179        elif state == 1:
180            state = 2
181        elif state == 2:
182            if line == '#include <...> search starts here:':
183                state = 3
184        elif state == 3:
185            if line == 'End of search list.':
186                state = 4
187            else:
188                paths.append(line.strip())
189    # now read built-in macros
190    state = 0
191    for line in out:
192        line = line.rstrip()
193        if state == 0:
194            if line == '# 1 "<built-in>"' or line == '# 1 "<command line>"':
195                state = 1
196        elif state == 1:
197            if line.startswith('#define '):
198                tokens = line[8:].split(' ', 1)
199                if len(tokens) == 1: tokens.append('')
200                macros.append(tuple(tokens))
201            elif line == '# 1 "%s"'%temp:
202                state = 0
203
204    # Per-compiler adjustments
205    for name, value in tuple(macros):
206        if name == '__GNUC__' and value == '2':
207            # gcc 2.x needs this or it uses nonstandard syntax in the headers
208            macros.append(('__STRICT_ANSI__', ''))
209
210    return True, paths, macros
211
212
213def find_compiler_info(language, compiler, flags):
214
215    found, paths, macros = False, [], []
216
217    if compiler == 'cl' and os.name == 'nt':
218        if flags:
219            sys.stderr.write('Warning: ignoring unknown flags for MSVC compiler\n')
220        found, paths, macros = find_ms_compiler_info()
221
222    else:
223        found, paths, macros = find_gcc_compiler_info(language, compiler, flags)
224
225    return found, paths, macros
226
227
228def get_compiler_timestamp(compiler):
229    """Returns the timestamp for the given compiler, or 0 if not found"""
230
231    path = os.getenv('PATH', os.defpath)
232    path = string.split(path, os.pathsep)
233    for directory in path:
234        # Try to stat the compiler in this directory, if it exists
235        filename = os.path.join(directory, compiler)
236        if os.name == 'nt': filename += '.exe'
237        try: stats = os.stat(filename)
238        except OSError: continue
239        return stats[stat.ST_CTIME]
240    # Not found
241    return 0
242
243
244class CompilerInfo:
245    """Info about one compiler."""
246
247    compiler = ''
248    """
249    The name of the compiler, typically the executable name,
250    which must either be in the path or given as an absolute,
251    pathname."""
252    flags = []
253    "Compiler flags that impact its characteristics."
254    language = ''
255    "The programming language the compiler is used for."
256    kind = ''
257    """
258    A string indicating the type of this info:
259    one of 'system', 'custom', ''.
260    'custom' compilers will never be automatically updated,
261    and an empty string indicates a failure to look up the 
262    given compiler."""
263    timestamp = ''
264    "The timestamp of the compiler binary."
265    include_paths = []
266    "A list of strings indicating the include paths."
267    macros = []
268    """
269    A list of (name,value) pairs. Values may be empty, or None.
270    The latter ase indicates that the macro is to be undefined."""
271
272    def _write(self, os):
273        item = id(self) >= 0 and id(self) or -id(self)
274        os.write('class Item%u:\n'%item)
275        for name, value in CompilerInfo.__dict__.iteritems():
276            if name[0] != '_':
277                os.write('    %s=%r\n'%(name, getattr(self, name)))
278        os.write('\n')
279
280
281class CompilerList(object):
282
283    user_emulations_file = '~/.synopsis/parsers/cpp/emulator'
284    "The cache file."
285
286    def __init__(self, filename = ''):
287
288        self.compilers = []
289        self.no_cache = os.environ.has_key('SYNOPSIS_NO_CACHE')
290        self.load(filename)
291
292    def list(self):
293
294        return [c.compiler for c in self.compilers]
295
296
297    def _query(self, language, compiler, flags):
298        """Construct and return a CompilerInfo object for the given compiler."""
299
300        ci = CompilerInfo()
301        ci.compiler = compiler
302        ci.flags = flags
303        ci.language = language
304        try:
305            found, paths, macros = find_compiler_info(language, compiler, flags)
306            if found:
307                ci.kind = 'system'
308                ci.timestamp = get_compiler_timestamp(compiler)
309                ci.include_paths = paths
310                ci.macros = macros
311        except:
312            ci.kind = '' # failure
313            ci.timestamp = 0
314            ci.include_paths = []
315            ci.macros = []
316        return ci
317
318    def add_default_compilers(self):
319
320        self.compilers.append(self._query('C++', 'c++', []))
321        self.compilers.append(self._query('C++', 'g++', []))
322        self.compilers.append(self._query('C', 'cc', []))
323        self.compilers.append(self._query('C', 'gcc', []))
324
325
326    def load(self, filename = ''):
327        """Loads the compiler infos from a file."""
328
329        if self.no_cache:
330            self.add_default_compilers()
331            return
332
333        compilers = []
334
335        glob = {}
336        glob['version'] = version
337        class Type(type):
338            """Factory for CompilerInfo objects.
339            This is used to read in an emulation file."""
340
341            def __init__(cls, name, bases, dict):
342
343                if glob['version'] == version:
344                    compiler = CompilerInfo()
345                    for name, value in CompilerInfo.__dict__.items():
346                        if name[0] != '_':
347                            setattr(compiler, name, dict.get(name, value))
348                    compilers.append(compiler)
349
350
351        if not filename:
352            filename = CompilerList.user_emulations_file
353        filename = os.path.expanduser(filename)
354        glob['__builtins__'] = __builtins__
355        glob['__name__'] = '__main__'
356        glob['__metaclass__'] = Type
357        try:
358            execfile(filename, glob, glob)
359        except IOError:
360
361            self.add_default_compilers()
362            self.save()
363        else:
364            self.compilers = compilers
365
366    def save(self, filename = ''):
367
368        if self.no_cache:
369            return
370
371        if not filename:
372            filename = CompilerList.user_emulations_file
373        filename = os.path.expanduser(filename)
374        dirname = os.path.dirname(filename)
375        if not os.path.exists(dirname):
376            os.makedirs(dirname)
377        emu = open(filename, 'wt')
378        emu.write("""# This file was generated by Synopsis.Parsers.Cpp.Emulator.
379# When making any manual modifications to any of the classes
380# be sure to set the 'kind' field to 'custom' so it doesn't get
381# accidentally overwritten !\n""")
382        emu.write('\n')
383        emu.write('version=%r\n'%version)
384        emu.write('\n')
385        for c in self.compilers:
386            c._write(emu)
387
388
389    def refresh(self):
390        """Refreshes the compiler list.
391        Regenerate all non-custom compilers without destroying custom
392        compilers."""
393
394        compilers = []
395        for ci in self.compilers:
396            if ci.is_custom:
397                compilers.append(ci)
398            ci = _query(ci.language, ci.compiler, ci.flags)
399            if ci:
400                compilers.append(ci)
401
402        self.compilers = compilers
403        self.save()
404
405    def find(self, language, compiler, flags):
406
407        if not flags:
408            flags = []
409        for ci in self.compilers:
410            if (not compiler and language == ci.language
411                or (compiler == ci.compiler and flags == ci.flags)):
412                return ci
413        ci = self._query(language, compiler, flags)
414        self.compilers.append(ci)
415        self.save()
416        return ci
417
418
419# Cache that makes multiple calls to 'get_compiler_info' more efficient.
420compiler_list = None
421
422
423def get_compiler_info(language, compiler = '', flags = None):
424    """
425    Returns the compiler info for the given compiler. If none is
426    specified (''), return the first available one for the given language.
427    The info is returned as a CompilerInfo object, or None if the compiler
428    isn't found. 
429    """
430    global compiler_list
431
432    if not compiler_list:
433        compiler_list = CompilerList()
434
435    ci = compiler_list.find(language, compiler, flags)
436    return ci
437