1import os
2import sys
3
4try:
5    basestring
6except NameError:
7    # Python 3.x
8    basestring = str
9
10def error(msg):
11    from distutils.errors import DistutilsSetupError
12    raise DistutilsSetupError(msg)
13
14
15def execfile(filename, glob):
16    # We use execfile() (here rewritten for Python 3) instead of
17    # __import__() to load the build script.  The problem with
18    # a normal import is that in some packages, the intermediate
19    # __init__.py files may already try to import the file that
20    # we are generating.
21    with open(filename) as f:
22        src = f.read()
23    src += '\n'      # Python 2.6 compatibility
24    code = compile(src, filename, 'exec')
25    exec(code, glob, glob)
26
27
28def add_cffi_module(dist, mod_spec):
29    from cffi.api import FFI
30
31    if not isinstance(mod_spec, basestring):
32        error("argument to 'cffi_modules=...' must be a str or a list of str,"
33              " not %r" % (type(mod_spec).__name__,))
34    mod_spec = str(mod_spec)
35    try:
36        build_file_name, ffi_var_name = mod_spec.split(':')
37    except ValueError:
38        error("%r must be of the form 'path/build.py:ffi_variable'" %
39              (mod_spec,))
40    if not os.path.exists(build_file_name):
41        ext = ''
42        rewritten = build_file_name.replace('.', '/') + '.py'
43        if os.path.exists(rewritten):
44            ext = ' (rewrite cffi_modules to [%r])' % (
45                rewritten + ':' + ffi_var_name,)
46        error("%r does not name an existing file%s" % (build_file_name, ext))
47
48    mod_vars = {'__name__': '__cffi__', '__file__': build_file_name}
49    execfile(build_file_name, mod_vars)
50
51    try:
52        ffi = mod_vars[ffi_var_name]
53    except KeyError:
54        error("%r: object %r not found in module" % (mod_spec,
55                                                     ffi_var_name))
56    if not isinstance(ffi, FFI):
57        ffi = ffi()      # maybe it's a function instead of directly an ffi
58    if not isinstance(ffi, FFI):
59        error("%r is not an FFI instance (got %r)" % (mod_spec,
60                                                      type(ffi).__name__))
61    if not hasattr(ffi, '_assigned_source'):
62        error("%r: the set_source() method was not called" % (mod_spec,))
63    module_name, source, source_extension, kwds = ffi._assigned_source
64    if ffi._windows_unicode:
65        kwds = kwds.copy()
66        ffi._apply_windows_unicode(kwds)
67
68    if source is None:
69        _add_py_module(dist, ffi, module_name)
70    else:
71        _add_c_module(dist, ffi, module_name, source, source_extension, kwds)
72
73def _set_py_limited_api(Extension, kwds):
74    """
75    Add py_limited_api to kwds if setuptools >= 26 is in use.
76    Do not alter the setting if it already exists.
77    Setuptools takes care of ignoring the flag on Python 2 and PyPy.
78
79    CPython itself should ignore the flag in a debugging version
80    (by not listing .abi3.so in the extensions it supports), but
81    it doesn't so far, creating troubles.  That's why we check
82    for "not hasattr(sys, 'gettotalrefcount')" (the 2.7 compatible equivalent
83    of 'd' not in sys.abiflags). (http://bugs.python.org/issue28401)
84
85    On Windows, with CPython <= 3.4, it's better not to use py_limited_api
86    because virtualenv *still* doesn't copy PYTHON3.DLL on these versions.
87    Recently (2020) we started shipping only >= 3.5 wheels, though.  So
88    we'll give it another try and set py_limited_api on Windows >= 3.5.
89    """
90    from cffi import recompiler
91
92    if ('py_limited_api' not in kwds and not hasattr(sys, 'gettotalrefcount')
93            and recompiler.USE_LIMITED_API):
94        import setuptools
95        try:
96            setuptools_major_version = int(setuptools.__version__.partition('.')[0])
97            if setuptools_major_version >= 26:
98                kwds['py_limited_api'] = True
99        except ValueError:  # certain development versions of setuptools
100            # If we don't know the version number of setuptools, we
101            # try to set 'py_limited_api' anyway.  At worst, we get a
102            # warning.
103            kwds['py_limited_api'] = True
104    return kwds
105
106def _add_c_module(dist, ffi, module_name, source, source_extension, kwds):
107    from distutils.core import Extension
108    # We are a setuptools extension. Need this build_ext for py_limited_api.
109    from setuptools.command.build_ext import build_ext
110    from distutils.dir_util import mkpath
111    from distutils import log
112    from cffi import recompiler
113
114    allsources = ['$PLACEHOLDER']
115    allsources.extend(kwds.pop('sources', []))
116    kwds = _set_py_limited_api(Extension, kwds)
117    ext = Extension(name=module_name, sources=allsources, **kwds)
118
119    def make_mod(tmpdir, pre_run=None):
120        c_file = os.path.join(tmpdir, module_name + source_extension)
121        log.info("generating cffi module %r" % c_file)
122        mkpath(tmpdir)
123        # a setuptools-only, API-only hook: called with the "ext" and "ffi"
124        # arguments just before we turn the ffi into C code.  To use it,
125        # subclass the 'distutils.command.build_ext.build_ext' class and
126        # add a method 'def pre_run(self, ext, ffi)'.
127        if pre_run is not None:
128            pre_run(ext, ffi)
129        updated = recompiler.make_c_source(ffi, module_name, source, c_file)
130        if not updated:
131            log.info("already up-to-date")
132        return c_file
133
134    if dist.ext_modules is None:
135        dist.ext_modules = []
136    dist.ext_modules.append(ext)
137
138    base_class = dist.cmdclass.get('build_ext', build_ext)
139    class build_ext_make_mod(base_class):
140        def run(self):
141            if ext.sources[0] == '$PLACEHOLDER':
142                pre_run = getattr(self, 'pre_run', None)
143                ext.sources[0] = make_mod(self.build_temp, pre_run)
144            base_class.run(self)
145    dist.cmdclass['build_ext'] = build_ext_make_mod
146    # NB. multiple runs here will create multiple 'build_ext_make_mod'
147    # classes.  Even in this case the 'build_ext' command should be
148    # run once; but just in case, the logic above does nothing if
149    # called again.
150
151
152def _add_py_module(dist, ffi, module_name):
153    from distutils.dir_util import mkpath
154    from setuptools.command.build_py import build_py
155    from setuptools.command.build_ext import build_ext
156    from distutils import log
157    from cffi import recompiler
158
159    def generate_mod(py_file):
160        log.info("generating cffi module %r" % py_file)
161        mkpath(os.path.dirname(py_file))
162        updated = recompiler.make_py_source(ffi, module_name, py_file)
163        if not updated:
164            log.info("already up-to-date")
165
166    base_class = dist.cmdclass.get('build_py', build_py)
167    class build_py_make_mod(base_class):
168        def run(self):
169            base_class.run(self)
170            module_path = module_name.split('.')
171            module_path[-1] += '.py'
172            generate_mod(os.path.join(self.build_lib, *module_path))
173        def get_source_files(self):
174            # This is called from 'setup.py sdist' only.  Exclude
175            # the generate .py module in this case.
176            saved_py_modules = self.py_modules
177            try:
178                if saved_py_modules:
179                    self.py_modules = [m for m in saved_py_modules
180                                         if m != module_name]
181                return base_class.get_source_files(self)
182            finally:
183                self.py_modules = saved_py_modules
184    dist.cmdclass['build_py'] = build_py_make_mod
185
186    # distutils and setuptools have no notion I could find of a
187    # generated python module.  If we don't add module_name to
188    # dist.py_modules, then things mostly work but there are some
189    # combination of options (--root and --record) that will miss
190    # the module.  So we add it here, which gives a few apparently
191    # harmless warnings about not finding the file outside the
192    # build directory.
193    # Then we need to hack more in get_source_files(); see above.
194    if dist.py_modules is None:
195        dist.py_modules = []
196    dist.py_modules.append(module_name)
197
198    # the following is only for "build_ext -i"
199    base_class_2 = dist.cmdclass.get('build_ext', build_ext)
200    class build_ext_make_mod(base_class_2):
201        def run(self):
202            base_class_2.run(self)
203            if self.inplace:
204                # from get_ext_fullpath() in distutils/command/build_ext.py
205                module_path = module_name.split('.')
206                package = '.'.join(module_path[:-1])
207                build_py = self.get_finalized_command('build_py')
208                package_dir = build_py.get_package_dir(package)
209                file_name = module_path[-1] + '.py'
210                generate_mod(os.path.join(package_dir, file_name))
211    dist.cmdclass['build_ext'] = build_ext_make_mod
212
213def cffi_modules(dist, attr, value):
214    assert attr == 'cffi_modules'
215    if isinstance(value, basestring):
216        value = [value]
217
218    for cffi_module in value:
219        add_cffi_module(dist, cffi_module)
220