setuptools_ext.py 9.2 KB

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