220 lines
8.7 KiB
Python
220 lines
8.7 KiB
Python
|
import os
|
||
|
import sys
|
||
|
|
||
|
try:
|
||
|
basestring
|
||
|
except NameError:
|
||
|
# Python 3.x
|
||
|
basestring = str
|
||
|
|
||
|
def error(msg):
|
||
|
from distutils.errors import DistutilsSetupError
|
||
|
raise DistutilsSetupError(msg)
|
||
|
|
||
|
|
||
|
def execfile(filename, glob):
|
||
|
# We use execfile() (here rewritten for Python 3) instead of
|
||
|
# __import__() to load the build script. The problem with
|
||
|
# a normal import is that in some packages, the intermediate
|
||
|
# __init__.py files may already try to import the file that
|
||
|
# we are generating.
|
||
|
with open(filename) as f:
|
||
|
src = f.read()
|
||
|
src += '\n' # Python 2.6 compatibility
|
||
|
code = compile(src, filename, 'exec')
|
||
|
exec(code, glob, glob)
|
||
|
|
||
|
|
||
|
def add_cffi_module(dist, mod_spec):
|
||
|
from cffi.api import FFI
|
||
|
|
||
|
if not isinstance(mod_spec, basestring):
|
||
|
error("argument to 'cffi_modules=...' must be a str or a list of str,"
|
||
|
" not %r" % (type(mod_spec).__name__,))
|
||
|
mod_spec = str(mod_spec)
|
||
|
try:
|
||
|
build_file_name, ffi_var_name = mod_spec.split(':')
|
||
|
except ValueError:
|
||
|
error("%r must be of the form 'path/build.py:ffi_variable'" %
|
||
|
(mod_spec,))
|
||
|
if not os.path.exists(build_file_name):
|
||
|
ext = ''
|
||
|
rewritten = build_file_name.replace('.', '/') + '.py'
|
||
|
if os.path.exists(rewritten):
|
||
|
ext = ' (rewrite cffi_modules to [%r])' % (
|
||
|
rewritten + ':' + ffi_var_name,)
|
||
|
error("%r does not name an existing file%s" % (build_file_name, ext))
|
||
|
|
||
|
mod_vars = {'__name__': '__cffi__', '__file__': build_file_name}
|
||
|
execfile(build_file_name, mod_vars)
|
||
|
|
||
|
try:
|
||
|
ffi = mod_vars[ffi_var_name]
|
||
|
except KeyError:
|
||
|
error("%r: object %r not found in module" % (mod_spec,
|
||
|
ffi_var_name))
|
||
|
if not isinstance(ffi, FFI):
|
||
|
ffi = ffi() # maybe it's a function instead of directly an ffi
|
||
|
if not isinstance(ffi, FFI):
|
||
|
error("%r is not an FFI instance (got %r)" % (mod_spec,
|
||
|
type(ffi).__name__))
|
||
|
if not hasattr(ffi, '_assigned_source'):
|
||
|
error("%r: the set_source() method was not called" % (mod_spec,))
|
||
|
module_name, source, source_extension, kwds = ffi._assigned_source
|
||
|
if ffi._windows_unicode:
|
||
|
kwds = kwds.copy()
|
||
|
ffi._apply_windows_unicode(kwds)
|
||
|
|
||
|
if source is None:
|
||
|
_add_py_module(dist, ffi, module_name)
|
||
|
else:
|
||
|
_add_c_module(dist, ffi, module_name, source, source_extension, kwds)
|
||
|
|
||
|
def _set_py_limited_api(Extension, kwds):
|
||
|
"""
|
||
|
Add py_limited_api to kwds if setuptools >= 26 is in use.
|
||
|
Do not alter the setting if it already exists.
|
||
|
Setuptools takes care of ignoring the flag on Python 2 and PyPy.
|
||
|
|
||
|
CPython itself should ignore the flag in a debugging version
|
||
|
(by not listing .abi3.so in the extensions it supports), but
|
||
|
it doesn't so far, creating troubles. That's why we check
|
||
|
for "not hasattr(sys, 'gettotalrefcount')" (the 2.7 compatible equivalent
|
||
|
of 'd' not in sys.abiflags). (http://bugs.python.org/issue28401)
|
||
|
|
||
|
On Windows, with CPython <= 3.4, it's better not to use py_limited_api
|
||
|
because virtualenv *still* doesn't copy PYTHON3.DLL on these versions.
|
||
|
Recently (2020) we started shipping only >= 3.5 wheels, though. So
|
||
|
we'll give it another try and set py_limited_api on Windows >= 3.5.
|
||
|
"""
|
||
|
from cffi import recompiler
|
||
|
|
||
|
if ('py_limited_api' not in kwds and not hasattr(sys, 'gettotalrefcount')
|
||
|
and recompiler.USE_LIMITED_API):
|
||
|
import setuptools
|
||
|
try:
|
||
|
setuptools_major_version = int(setuptools.__version__.partition('.')[0])
|
||
|
if setuptools_major_version >= 26:
|
||
|
kwds['py_limited_api'] = True
|
||
|
except ValueError: # certain development versions of setuptools
|
||
|
# If we don't know the version number of setuptools, we
|
||
|
# try to set 'py_limited_api' anyway. At worst, we get a
|
||
|
# warning.
|
||
|
kwds['py_limited_api'] = True
|
||
|
return kwds
|
||
|
|
||
|
def _add_c_module(dist, ffi, module_name, source, source_extension, kwds):
|
||
|
from distutils.core import Extension
|
||
|
# We are a setuptools extension. Need this build_ext for py_limited_api.
|
||
|
from setuptools.command.build_ext import build_ext
|
||
|
from distutils.dir_util import mkpath
|
||
|
from distutils import log
|
||
|
from cffi import recompiler
|
||
|
|
||
|
allsources = ['$PLACEHOLDER']
|
||
|
allsources.extend(kwds.pop('sources', []))
|
||
|
kwds = _set_py_limited_api(Extension, kwds)
|
||
|
ext = Extension(name=module_name, sources=allsources, **kwds)
|
||
|
|
||
|
def make_mod(tmpdir, pre_run=None):
|
||
|
c_file = os.path.join(tmpdir, module_name + source_extension)
|
||
|
log.info("generating cffi module %r" % c_file)
|
||
|
mkpath(tmpdir)
|
||
|
# a setuptools-only, API-only hook: called with the "ext" and "ffi"
|
||
|
# arguments just before we turn the ffi into C code. To use it,
|
||
|
# subclass the 'distutils.command.build_ext.build_ext' class and
|
||
|
# add a method 'def pre_run(self, ext, ffi)'.
|
||
|
if pre_run is not None:
|
||
|
pre_run(ext, ffi)
|
||
|
updated = recompiler.make_c_source(ffi, module_name, source, c_file)
|
||
|
if not updated:
|
||
|
log.info("already up-to-date")
|
||
|
return c_file
|
||
|
|
||
|
if dist.ext_modules is None:
|
||
|
dist.ext_modules = []
|
||
|
dist.ext_modules.append(ext)
|
||
|
|
||
|
base_class = dist.cmdclass.get('build_ext', build_ext)
|
||
|
class build_ext_make_mod(base_class):
|
||
|
def run(self):
|
||
|
if ext.sources[0] == '$PLACEHOLDER':
|
||
|
pre_run = getattr(self, 'pre_run', None)
|
||
|
ext.sources[0] = make_mod(self.build_temp, pre_run)
|
||
|
base_class.run(self)
|
||
|
dist.cmdclass['build_ext'] = build_ext_make_mod
|
||
|
# NB. multiple runs here will create multiple 'build_ext_make_mod'
|
||
|
# classes. Even in this case the 'build_ext' command should be
|
||
|
# run once; but just in case, the logic above does nothing if
|
||
|
# called again.
|
||
|
|
||
|
|
||
|
def _add_py_module(dist, ffi, module_name):
|
||
|
from distutils.dir_util import mkpath
|
||
|
from setuptools.command.build_py import build_py
|
||
|
from setuptools.command.build_ext import build_ext
|
||
|
from distutils import log
|
||
|
from cffi import recompiler
|
||
|
|
||
|
def generate_mod(py_file):
|
||
|
log.info("generating cffi module %r" % py_file)
|
||
|
mkpath(os.path.dirname(py_file))
|
||
|
updated = recompiler.make_py_source(ffi, module_name, py_file)
|
||
|
if not updated:
|
||
|
log.info("already up-to-date")
|
||
|
|
||
|
base_class = dist.cmdclass.get('build_py', build_py)
|
||
|
class build_py_make_mod(base_class):
|
||
|
def run(self):
|
||
|
base_class.run(self)
|
||
|
module_path = module_name.split('.')
|
||
|
module_path[-1] += '.py'
|
||
|
generate_mod(os.path.join(self.build_lib, *module_path))
|
||
|
def get_source_files(self):
|
||
|
# This is called from 'setup.py sdist' only. Exclude
|
||
|
# the generate .py module in this case.
|
||
|
saved_py_modules = self.py_modules
|
||
|
try:
|
||
|
if saved_py_modules:
|
||
|
self.py_modules = [m for m in saved_py_modules
|
||
|
if m != module_name]
|
||
|
return base_class.get_source_files(self)
|
||
|
finally:
|
||
|
self.py_modules = saved_py_modules
|
||
|
dist.cmdclass['build_py'] = build_py_make_mod
|
||
|
|
||
|
# distutils and setuptools have no notion I could find of a
|
||
|
# generated python module. If we don't add module_name to
|
||
|
# dist.py_modules, then things mostly work but there are some
|
||
|
# combination of options (--root and --record) that will miss
|
||
|
# the module. So we add it here, which gives a few apparently
|
||
|
# harmless warnings about not finding the file outside the
|
||
|
# build directory.
|
||
|
# Then we need to hack more in get_source_files(); see above.
|
||
|
if dist.py_modules is None:
|
||
|
dist.py_modules = []
|
||
|
dist.py_modules.append(module_name)
|
||
|
|
||
|
# the following is only for "build_ext -i"
|
||
|
base_class_2 = dist.cmdclass.get('build_ext', build_ext)
|
||
|
class build_ext_make_mod(base_class_2):
|
||
|
def run(self):
|
||
|
base_class_2.run(self)
|
||
|
if self.inplace:
|
||
|
# from get_ext_fullpath() in distutils/command/build_ext.py
|
||
|
module_path = module_name.split('.')
|
||
|
package = '.'.join(module_path[:-1])
|
||
|
build_py = self.get_finalized_command('build_py')
|
||
|
package_dir = build_py.get_package_dir(package)
|
||
|
file_name = module_path[-1] + '.py'
|
||
|
generate_mod(os.path.join(package_dir, file_name))
|
||
|
dist.cmdclass['build_ext'] = build_ext_make_mod
|
||
|
|
||
|
def cffi_modules(dist, attr, value):
|
||
|
assert attr == 'cffi_modules'
|
||
|
if isinstance(value, basestring):
|
||
|
value = [value]
|
||
|
|
||
|
for cffi_module in value:
|
||
|
add_cffi_module(dist, cffi_module)
|