1#
2# DEPRECATED: implementation for ffi.verify()
3#
4import sys, os, binascii, shutil, io
5from . import __version_verifier_modules__
6from . import ffiplatform
7from .error import VerificationError
8
9if sys.version_info >= (3, 3):
10    import importlib.machinery
11    def _extension_suffixes():
12        return importlib.machinery.EXTENSION_SUFFIXES[:]
13else:
14    import imp
15    def _extension_suffixes():
16        return [suffix for suffix, _, type in imp.get_suffixes()
17                if type == imp.C_EXTENSION]
18
19
20if sys.version_info >= (3,):
21    NativeIO = io.StringIO
22else:
23    class NativeIO(io.BytesIO):
24        def write(self, s):
25            if isinstance(s, unicode):
26                s = s.encode('ascii')
27            super(NativeIO, self).write(s)
28
29
30class Verifier(object):
31
32    def __init__(self, ffi, preamble, tmpdir=None, modulename=None,
33                 ext_package=None, tag='', force_generic_engine=False,
34                 source_extension='.c', flags=None, relative_to=None, **kwds):
35        if ffi._parser._uses_new_feature:
36            raise VerificationError(
37                "feature not supported with ffi.verify(), but only "
38                "with ffi.set_source(): %s" % (ffi._parser._uses_new_feature,))
39        self.ffi = ffi
40        self.preamble = preamble
41        if not modulename:
42            flattened_kwds = ffiplatform.flatten(kwds)
43        vengine_class = _locate_engine_class(ffi, force_generic_engine)
44        self._vengine = vengine_class(self)
45        self._vengine.patch_extension_kwds(kwds)
46        self.flags = flags
47        self.kwds = self.make_relative_to(kwds, relative_to)
48        #
49        if modulename:
50            if tag:
51                raise TypeError("can't specify both 'modulename' and 'tag'")
52        else:
53            key = '\x00'.join([sys.version[:3], __version_verifier_modules__,
54                               preamble, flattened_kwds] +
55                              ffi._cdefsources)
56            if sys.version_info >= (3,):
57                key = key.encode('utf-8')
58            k1 = hex(binascii.crc32(key[0::2]) & 0xffffffff)
59            k1 = k1.lstrip('0x').rstrip('L')
60            k2 = hex(binascii.crc32(key[1::2]) & 0xffffffff)
61            k2 = k2.lstrip('0').rstrip('L')
62            modulename = '_cffi_%s_%s%s%s' % (tag, self._vengine._class_key,
63                                              k1, k2)
64        suffix = _get_so_suffixes()[0]
65        self.tmpdir = tmpdir or _caller_dir_pycache()
66        self.sourcefilename = os.path.join(self.tmpdir, modulename + source_extension)
67        self.modulefilename = os.path.join(self.tmpdir, modulename + suffix)
68        self.ext_package = ext_package
69        self._has_source = False
70        self._has_module = False
71
72    def write_source(self, file=None):
73        """Write the C source code.  It is produced in 'self.sourcefilename',
74        which can be tweaked beforehand."""
75        with self.ffi._lock:
76            if self._has_source and file is None:
77                raise VerificationError(
78                    "source code already written")
79            self._write_source(file)
80
81    def compile_module(self):
82        """Write the C source code (if not done already) and compile it.
83        This produces a dynamic link library in 'self.modulefilename'."""
84        with self.ffi._lock:
85            if self._has_module:
86                raise VerificationError("module already compiled")
87            if not self._has_source:
88                self._write_source()
89            self._compile_module()
90
91    def load_library(self):
92        """Get a C module from this Verifier instance.
93        Returns an instance of a FFILibrary class that behaves like the
94        objects returned by ffi.dlopen(), but that delegates all
95        operations to the C module.  If necessary, the C code is written
96        and compiled first.
97        """
98        with self.ffi._lock:
99            if not self._has_module:
100                self._locate_module()
101                if not self._has_module:
102                    if not self._has_source:
103                        self._write_source()
104                    self._compile_module()
105            return self._load_library()
106
107    def get_module_name(self):
108        basename = os.path.basename(self.modulefilename)
109        # kill both the .so extension and the other .'s, as introduced
110        # by Python 3: 'basename.cpython-33m.so'
111        basename = basename.split('.', 1)[0]
112        # and the _d added in Python 2 debug builds --- but try to be
113        # conservative and not kill a legitimate _d
114        if basename.endswith('_d') and hasattr(sys, 'gettotalrefcount'):
115            basename = basename[:-2]
116        return basename
117
118    def get_extension(self):
119        ffiplatform._hack_at_distutils() # backward compatibility hack
120        if not self._has_source:
121            with self.ffi._lock:
122                if not self._has_source:
123                    self._write_source()
124        sourcename = ffiplatform.maybe_relative_path(self.sourcefilename)
125        modname = self.get_module_name()
126        return ffiplatform.get_extension(sourcename, modname, **self.kwds)
127
128    def generates_python_module(self):
129        return self._vengine._gen_python_module
130
131    def make_relative_to(self, kwds, relative_to):
132        if relative_to and os.path.dirname(relative_to):
133            dirname = os.path.dirname(relative_to)
134            kwds = kwds.copy()
135            for key in ffiplatform.LIST_OF_FILE_NAMES:
136                if key in kwds:
137                    lst = kwds[key]
138                    if not isinstance(lst, (list, tuple)):
139                        raise TypeError("keyword '%s' should be a list or tuple"
140                                        % (key,))
141                    lst = [os.path.join(dirname, fn) for fn in lst]
142                    kwds[key] = lst
143        return kwds
144
145    # ----------
146
147    def _locate_module(self):
148        if not os.path.isfile(self.modulefilename):
149            if self.ext_package:
150                try:
151                    pkg = __import__(self.ext_package, None, None, ['__doc__'])
152                except ImportError:
153                    return      # cannot import the package itself, give up
154                    # (e.g. it might be called differently before installation)
155                path = pkg.__path__
156            else:
157                path = None
158            filename = self._vengine.find_module(self.get_module_name(), path,
159                                                 _get_so_suffixes())
160            if filename is None:
161                return
162            self.modulefilename = filename
163        self._vengine.collect_types()
164        self._has_module = True
165
166    def _write_source_to(self, file):
167        self._vengine._f = file
168        try:
169            self._vengine.write_source_to_f()
170        finally:
171            del self._vengine._f
172
173    def _write_source(self, file=None):
174        if file is not None:
175            self._write_source_to(file)
176        else:
177            # Write our source file to an in memory file.
178            f = NativeIO()
179            self._write_source_to(f)
180            source_data = f.getvalue()
181
182            # Determine if this matches the current file
183            if os.path.exists(self.sourcefilename):
184                with open(self.sourcefilename, "r") as fp:
185                    needs_written = not (fp.read() == source_data)
186            else:
187                needs_written = True
188
189            # Actually write the file out if it doesn't match
190            if needs_written:
191                _ensure_dir(self.sourcefilename)
192                with open(self.sourcefilename, "w") as fp:
193                    fp.write(source_data)
194
195            # Set this flag
196            self._has_source = True
197
198    def _compile_module(self):
199        # compile this C source
200        tmpdir = os.path.dirname(self.sourcefilename)
201        outputfilename = ffiplatform.compile(tmpdir, self.get_extension())
202        try:
203            same = ffiplatform.samefile(outputfilename, self.modulefilename)
204        except OSError:
205            same = False
206        if not same:
207            _ensure_dir(self.modulefilename)
208            shutil.move(outputfilename, self.modulefilename)
209        self._has_module = True
210
211    def _load_library(self):
212        assert self._has_module
213        if self.flags is not None:
214            return self._vengine.load_library(self.flags)
215        else:
216            return self._vengine.load_library()
217
218# ____________________________________________________________
219
220_FORCE_GENERIC_ENGINE = False      # for tests
221
222def _locate_engine_class(ffi, force_generic_engine):
223    if _FORCE_GENERIC_ENGINE:
224        force_generic_engine = True
225    if not force_generic_engine:
226        if '__pypy__' in sys.builtin_module_names:
227            force_generic_engine = True
228        else:
229            try:
230                import _cffi_backend
231            except ImportError:
232                _cffi_backend = '?'
233            if ffi._backend is not _cffi_backend:
234                force_generic_engine = True
235    if force_generic_engine:
236        from . import vengine_gen
237        return vengine_gen.VGenericEngine
238    else:
239        from . import vengine_cpy
240        return vengine_cpy.VCPythonEngine
241
242# ____________________________________________________________
243
244_TMPDIR = None
245
246def _caller_dir_pycache():
247    if _TMPDIR:
248        return _TMPDIR
249    result = os.environ.get('CFFI_TMPDIR')
250    if result:
251        return result
252    filename = sys._getframe(2).f_code.co_filename
253    return os.path.abspath(os.path.join(os.path.dirname(filename),
254                           '__pycache__'))
255
256def set_tmpdir(dirname):
257    """Set the temporary directory to use instead of __pycache__."""
258    global _TMPDIR
259    _TMPDIR = dirname
260
261def cleanup_tmpdir(tmpdir=None, keep_so=False):
262    """Clean up the temporary directory by removing all files in it
263    called `_cffi_*.{c,so}` as well as the `build` subdirectory."""
264    tmpdir = tmpdir or _caller_dir_pycache()
265    try:
266        filelist = os.listdir(tmpdir)
267    except OSError:
268        return
269    if keep_so:
270        suffix = '.c'   # only remove .c files
271    else:
272        suffix = _get_so_suffixes()[0].lower()
273    for fn in filelist:
274        if fn.lower().startswith('_cffi_') and (
275                fn.lower().endswith(suffix) or fn.lower().endswith('.c')):
276            try:
277                os.unlink(os.path.join(tmpdir, fn))
278            except OSError:
279                pass
280    clean_dir = [os.path.join(tmpdir, 'build')]
281    for dir in clean_dir:
282        try:
283            for fn in os.listdir(dir):
284                fn = os.path.join(dir, fn)
285                if os.path.isdir(fn):
286                    clean_dir.append(fn)
287                else:
288                    os.unlink(fn)
289        except OSError:
290            pass
291
292def _get_so_suffixes():
293    suffixes = _extension_suffixes()
294    if not suffixes:
295        # bah, no C_EXTENSION available.  Occurs on pypy without cpyext
296        if sys.platform == 'win32':
297            suffixes = [".pyd"]
298        else:
299            suffixes = [".so"]
300
301    return suffixes
302
303def _ensure_dir(filename):
304    dirname = os.path.dirname(filename)
305    if dirname and not os.path.isdir(dirname):
306        os.makedirs(dirname)
307