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