setuptools_ext.py 8.7 KB

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