• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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    For now we'll skip py_limited_api on all Windows versions to avoid an
88    inconsistent mess.
89    """
90    if ('py_limited_api' not in kwds and not hasattr(sys, 'gettotalrefcount')
91            and sys.platform != 'win32'):
92        import setuptools
93        try:
94            setuptools_major_version = int(setuptools.__version__.partition('.')[0])
95            if setuptools_major_version >= 26:
96                kwds['py_limited_api'] = True
97        except ValueError:  # certain development versions of setuptools
98            # If we don't know the version number of setuptools, we
99            # try to set 'py_limited_api' anyway.  At worst, we get a
100            # warning.
101            kwds['py_limited_api'] = True
102    return kwds
103
104def _add_c_module(dist, ffi, module_name, source, source_extension, kwds):
105    from distutils.core import Extension
106    # We are a setuptools extension. Need this build_ext for py_limited_api.
107    from setuptools.command.build_ext import build_ext
108    from distutils.dir_util import mkpath
109    from distutils import log
110    from cffi import recompiler
111
112    allsources = ['$PLACEHOLDER']
113    allsources.extend(kwds.pop('sources', []))
114    kwds = _set_py_limited_api(Extension, kwds)
115    ext = Extension(name=module_name, sources=allsources, **kwds)
116
117    def make_mod(tmpdir, pre_run=None):
118        c_file = os.path.join(tmpdir, module_name + source_extension)
119        log.info("generating cffi module %r" % c_file)
120        mkpath(tmpdir)
121        # a setuptools-only, API-only hook: called with the "ext" and "ffi"
122        # arguments just before we turn the ffi into C code.  To use it,
123        # subclass the 'distutils.command.build_ext.build_ext' class and
124        # add a method 'def pre_run(self, ext, ffi)'.
125        if pre_run is not None:
126            pre_run(ext, ffi)
127        updated = recompiler.make_c_source(ffi, module_name, source, c_file)
128        if not updated:
129            log.info("already up-to-date")
130        return c_file
131
132    if dist.ext_modules is None:
133        dist.ext_modules = []
134    dist.ext_modules.append(ext)
135
136    base_class = dist.cmdclass.get('build_ext', build_ext)
137    class build_ext_make_mod(base_class):
138        def run(self):
139            if ext.sources[0] == '$PLACEHOLDER':
140                pre_run = getattr(self, 'pre_run', None)
141                ext.sources[0] = make_mod(self.build_temp, pre_run)
142            base_class.run(self)
143    dist.cmdclass['build_ext'] = build_ext_make_mod
144    # NB. multiple runs here will create multiple 'build_ext_make_mod'
145    # classes.  Even in this case the 'build_ext' command should be
146    # run once; but just in case, the logic above does nothing if
147    # called again.
148
149
150def _add_py_module(dist, ffi, module_name):
151    from distutils.dir_util import mkpath
152    from setuptools.command.build_py import build_py
153    from setuptools.command.build_ext import build_ext
154    from distutils import log
155    from cffi import recompiler
156
157    def generate_mod(py_file):
158        log.info("generating cffi module %r" % py_file)
159        mkpath(os.path.dirname(py_file))
160        updated = recompiler.make_py_source(ffi, module_name, py_file)
161        if not updated:
162            log.info("already up-to-date")
163
164    base_class = dist.cmdclass.get('build_py', build_py)
165    class build_py_make_mod(base_class):
166        def run(self):
167            base_class.run(self)
168            module_path = module_name.split('.')
169            module_path[-1] += '.py'
170            generate_mod(os.path.join(self.build_lib, *module_path))
171        def get_source_files(self):
172            # This is called from 'setup.py sdist' only.  Exclude
173            # the generate .py module in this case.
174            saved_py_modules = self.py_modules
175            try:
176                if saved_py_modules:
177                    self.py_modules = [m for m in saved_py_modules
178                                         if m != module_name]
179                return base_class.get_source_files(self)
180            finally:
181                self.py_modules = saved_py_modules
182    dist.cmdclass['build_py'] = build_py_make_mod
183
184    # distutils and setuptools have no notion I could find of a
185    # generated python module.  If we don't add module_name to
186    # dist.py_modules, then things mostly work but there are some
187    # combination of options (--root and --record) that will miss
188    # the module.  So we add it here, which gives a few apparently
189    # harmless warnings about not finding the file outside the
190    # build directory.
191    # Then we need to hack more in get_source_files(); see above.
192    if dist.py_modules is None:
193        dist.py_modules = []
194    dist.py_modules.append(module_name)
195
196    # the following is only for "build_ext -i"
197    base_class_2 = dist.cmdclass.get('build_ext', build_ext)
198    class build_ext_make_mod(base_class_2):
199        def run(self):
200            base_class_2.run(self)
201            if self.inplace:
202                # from get_ext_fullpath() in distutils/command/build_ext.py
203                module_path = module_name.split('.')
204                package = '.'.join(module_path[:-1])
205                build_py = self.get_finalized_command('build_py')
206                package_dir = build_py.get_package_dir(package)
207                file_name = module_path[-1] + '.py'
208                generate_mod(os.path.join(package_dir, file_name))
209    dist.cmdclass['build_ext'] = build_ext_make_mod
210
211def cffi_modules(dist, attr, value):
212    assert attr == 'cffi_modules'
213    if isinstance(value, basestring):
214        value = [value]
215
216    for cffi_module in value:
217        add_cffi_module(dist, cffi_module)
218