setup_cython.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. #
  2. # Cython patcher
  3. # v1.0
  4. #
  5. # Copyright (C) 2012-2016 Michael Buesch <m@bues.ch>
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. from __future__ import print_function
  21. import sys
  22. import os
  23. import platform
  24. import errno
  25. import shutil
  26. import hashlib
  27. import re
  28. from distutils.core import setup
  29. from distutils.extension import Extension
  30. parallelBuild = False
  31. ext_modules = []
  32. CythonBuildExtension = None
  33. _Cython_Distutils_build_ext = None
  34. _cythonPossible = None
  35. _cythonBuildUnits = []
  36. def makedirs(path, mode=0o755):
  37. try:
  38. os.makedirs(path, mode)
  39. except OSError as e:
  40. if e.errno == errno.EEXIST:
  41. return
  42. raise e
  43. def hashFile(path):
  44. if sys.version_info[0] < 3:
  45. ExpectedException = IOError
  46. else:
  47. ExpectedException = FileNotFoundError
  48. try:
  49. return hashlib.sha1(open(path, "rb").read()).hexdigest()
  50. except ExpectedException as e:
  51. return None
  52. def __fileopIfChanged(fromFile, toFile, fileop):
  53. toFileHash = hashFile(toFile)
  54. if toFileHash is not None:
  55. fromFileHash = hashFile(fromFile)
  56. if toFileHash == fromFileHash:
  57. return False
  58. makedirs(os.path.dirname(toFile))
  59. fileop(fromFile, toFile)
  60. return True
  61. def copyIfChanged(fromFile, toFile):
  62. return __fileopIfChanged(fromFile, toFile, shutil.copy2)
  63. def moveIfChanged(fromFile, toFile):
  64. return __fileopIfChanged(fromFile, toFile, os.rename)
  65. def makeDummyFile(path):
  66. if os.path.isfile(path):
  67. return
  68. print("creating dummy file '%s'" % path)
  69. makedirs(os.path.dirname(path))
  70. fd = open(path, "w")
  71. fd.write("\n")
  72. fd.close()
  73. def pyCythonPatchLine(line, basicOnly=False):
  74. return line
  75. def pyCythonPatch(fromFile, toFile, basicOnly=False):
  76. print("cython-patch: patching file '%s' to '%s'" %\
  77. (fromFile, toFile))
  78. tmpFile = toFile + ".TMP"
  79. makedirs(os.path.dirname(tmpFile))
  80. infd = open(fromFile, "r")
  81. outfd = open(tmpFile, "w")
  82. for line in infd.readlines():
  83. stripLine = line.strip()
  84. if stripLine.endswith("#<no-cython-patch"):
  85. outfd.write(line)
  86. continue
  87. # Uncomment all lines containing #@cy
  88. if "#@cy" in stripLine:
  89. line = line.replace("#@cy", "")
  90. if line.startswith("#"):
  91. line = line[1:]
  92. if not line.endswith("\n"):
  93. line += "\n"
  94. # Sprinkle magic cdef, as requested by #+cdef
  95. if "#+cdef" in stripLine:
  96. if stripLine.startswith("class"):
  97. line = line.replace("class", "cdef class")
  98. else:
  99. line = line.replace("def", "cdef")
  100. # Comment all lines containing #@nocy
  101. if "#@nocy" in stripLine:
  102. line = "#" + line
  103. if not basicOnly:
  104. # Automagic types
  105. line = re.sub(r'\b_Bool\b', "unsigned char", line)
  106. line = re.sub(r'\bint8_t\b', "signed char", line)
  107. line = re.sub(r'\buint8_t\b', "unsigned char", line)
  108. line = re.sub(r'\bint16_t\b', "signed short", line)
  109. line = re.sub(r'\buint16_t\b', "unsigned short", line)
  110. line = re.sub(r'\bint32_t\b', "signed int", line)
  111. line = re.sub(r'\buint32_t\b', "unsigned int", line)
  112. line = re.sub(r'\bint64_t\b', "signed long long", line)
  113. line = re.sub(r'\buint64_t\b', "unsigned long long", line)
  114. # Remove compat stuff
  115. line = line.replace("absolute_import,", "")
  116. line = pyCythonPatchLine(line, basicOnly)
  117. outfd.write(line)
  118. infd.close()
  119. outfd.flush()
  120. outfd.close()
  121. if not moveIfChanged(tmpFile, toFile):
  122. print("(already up to date)")
  123. os.unlink(tmpFile)
  124. class CythonBuildUnit(object):
  125. def __init__(self, cyModName, baseName, fromPy, fromPxd, toDir, toPyx, toPxd):
  126. self.cyModName = cyModName
  127. self.baseName = baseName
  128. self.fromPy = fromPy
  129. self.fromPxd = fromPxd
  130. self.toDir = toDir
  131. self.toPyx = toPyx
  132. self.toPxd = toPxd
  133. def patchCythonModules(buildDir):
  134. for unit in _cythonBuildUnits:
  135. makedirs(unit.toDir)
  136. makeDummyFile(os.path.join(unit.toDir, "__init__.py"))
  137. if unit.baseName == "__init__":
  138. # Copy and patch the package __init__.py
  139. toPy = os.path.join(buildDir, *unit.cyModName.split(".")) + ".py"
  140. pyCythonPatch(unit.fromPy, toPy,
  141. basicOnly=True)
  142. else:
  143. # Generate the .pyx
  144. pyCythonPatch(unit.fromPy, unit.toPyx)
  145. # Copy and patch the .pxd, if any
  146. if os.path.isfile(unit.fromPxd):
  147. pyCythonPatch(unit.fromPxd, unit.toPxd)
  148. def registerCythonModule(baseDir, sourceModName):
  149. global ext_modules
  150. global _cythonBuildUnits
  151. modDir = os.path.join(baseDir, sourceModName)
  152. # Make path to the cython patch-build-dir
  153. patchDir = os.path.join(baseDir, "build",
  154. "cython_patched.%s-%s-%d.%d" %\
  155. (platform.system().lower(),
  156. platform.machine().lower(),
  157. sys.version_info[0], sys.version_info[1]),
  158. "%s_cython" % sourceModName
  159. )
  160. if not os.path.exists(os.path.join(baseDir, "setup.py")) or\
  161. not os.path.exists(modDir) or\
  162. not os.path.isdir(modDir):
  163. raise Exception("Wrong directory. "
  164. "Execute setup.py from within the main directory.")
  165. # Walk the module
  166. for dirpath, dirnames, filenames in os.walk(modDir):
  167. subpath = os.path.relpath(dirpath, modDir)
  168. if subpath == baseDir:
  169. subpath = ""
  170. dirpathList = dirpath.split(os.path.sep)
  171. if any(os.path.exists(os.path.sep.join(dirpathList[:i] + ["no_cython"]))
  172. for i in range(len(dirpathList) + 1)):
  173. # no_cython file exists. -> skip
  174. continue
  175. for filename in filenames:
  176. if not filename.endswith(".py"):
  177. continue
  178. baseName = filename[:-3] # Strip .py
  179. fromPy = os.path.join(dirpath, baseName + ".py")
  180. fromPxd = os.path.join(dirpath, baseName + ".pxd.in")
  181. toDir = os.path.join(patchDir, subpath)
  182. toPyx = os.path.join(toDir, baseName + ".pyx")
  183. toPxd = os.path.join(toDir, baseName + ".pxd")
  184. # Construct the new cython module name
  185. cyModName = [ "%s_cython" % sourceModName ]
  186. if subpath:
  187. cyModName.extend(subpath.split(os.sep))
  188. cyModName.append(baseName)
  189. cyModName = ".".join(cyModName)
  190. # Remember the filenames for the build
  191. unit = CythonBuildUnit(cyModName, baseName, fromPy, fromPxd,
  192. toDir, toPyx, toPxd)
  193. _cythonBuildUnits.append(unit)
  194. if baseName != "__init__":
  195. # Create a distutils Extension for the module
  196. ext_modules.append(
  197. Extension(cyModName, [toPyx])
  198. )
  199. def registerCythonModules():
  200. baseDir = os.curdir # Base directory, where setup.py lives.
  201. for filename in os.listdir(baseDir):
  202. if os.path.isdir(os.path.join(baseDir, filename)) and\
  203. os.path.exists(os.path.join(baseDir, filename, "__init__.py")) and\
  204. not os.path.exists(os.path.join(baseDir, filename, "no_cython")):
  205. registerCythonModule(baseDir, filename)
  206. def cythonBuildPossible():
  207. global _cythonPossible
  208. if _cythonPossible is not None:
  209. return _cythonPossible
  210. _cythonPossible = False
  211. if os.name != "posix":
  212. print("WARNING: Not building CYTHON modules on '%s' platform." %\
  213. os.name)
  214. return False
  215. if "bdist_wininst" in sys.argv:
  216. print("WARNING: Omitting CYTHON modules while building "
  217. "Windows installer.")
  218. return False
  219. try:
  220. from Cython.Distutils import build_ext
  221. global _Cython_Distutils_build_ext
  222. _Cython_Distutils_build_ext = build_ext
  223. except ImportError as e:
  224. print("WARNING: Could not build the CYTHON modules: "
  225. "%s" % str(e))
  226. print("--> Is Cython installed?")
  227. return False
  228. _cythonPossible = True
  229. return True
  230. if sys.version_info[0] < 3:
  231. # Cython2 build libraries need method pickling
  232. # for parallel build.
  233. def unpickle_method(fname, obj, cls):
  234. # Ignore MRO. We don't seem to inherit methods.
  235. return cls.__dict__[fname].__get__(obj, cls)
  236. def pickle_method(m):
  237. return unpickle_method, (m.im_func.__name__,
  238. m.im_self,
  239. m.im_class)
  240. import copy_reg, types
  241. copy_reg.pickle(types.MethodType, pickle_method, unpickle_method)
  242. def cyBuildWrapper(arg):
  243. # This function does the same thing as the for-loop-body
  244. # inside of Cython's build_ext.build_extensions() method.
  245. # It is called via multiprocessing to build extensions
  246. # in parallel.
  247. # Note that this might break, if Cython's build_extensions()
  248. # is changed and stuff is added to its for loop. Meh.
  249. self, ext = arg
  250. ext.sources = self.cython_sources(ext.sources, ext)
  251. self.build_extension(ext)
  252. if cythonBuildPossible():
  253. # Override Cython's build_ext class.
  254. class CythonBuildExtension(_Cython_Distutils_build_ext):
  255. def build_extension(self, ext):
  256. assert(not ext.name.endswith("__init__"))
  257. _Cython_Distutils_build_ext.build_extension(self, ext)
  258. def build_extensions(self):
  259. global parallelBuild
  260. # First patch the files, the run the build
  261. patchCythonModules(self.build_lib)
  262. if parallelBuild:
  263. # Run the parallel build, yay.
  264. try:
  265. self.check_extensions_list(self.extensions)
  266. from multiprocessing.pool import Pool
  267. Pool().map(cyBuildWrapper,
  268. ((self, ext) for ext in self.extensions))
  269. except OSError as e:
  270. # This might happen in a restricted
  271. # environment like chroot.
  272. print("WARNING: Parallel build "
  273. "disabled due to: %s" % str(e))
  274. parallelBuild = False
  275. if not parallelBuild:
  276. # Run the normal non-parallel build.
  277. _Cython_Distutils_build_ext.build_extensions(self)