cython.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. #! /usr/bin/env python
  2. # encoding: utf-8
  3. # Thomas Nagy, 2010-2015
  4. import re
  5. from waflib import Task, Logs
  6. from waflib.TaskGen import extension
  7. cy_api_pat = re.compile(r'\s*?cdef\s*?(public|api)\w*')
  8. re_cyt = re.compile(r"""
  9. (?:from\s+(\w+)\s+)? # optionally match "from foo" and capture foo
  10. c?import\s(\w+|[*]) # require "import bar" and capture bar
  11. """, re.M | re.VERBOSE)
  12. @extension('.pyx')
  13. def add_cython_file(self, node):
  14. """
  15. Process a *.pyx* file given in the list of source files. No additional
  16. feature is required::
  17. def build(bld):
  18. bld(features='c cshlib pyext', source='main.c foo.pyx', target='app')
  19. """
  20. ext = '.c'
  21. if 'cxx' in self.features:
  22. self.env.append_unique('CYTHONFLAGS', '--cplus')
  23. ext = '.cc'
  24. for x in getattr(self, 'cython_includes', []):
  25. # TODO re-use these nodes in "scan" below
  26. d = self.path.find_dir(x)
  27. if d:
  28. self.env.append_unique('CYTHONFLAGS', '-I%s' % d.abspath())
  29. tsk = self.create_task('cython', node, node.change_ext(ext))
  30. self.source += tsk.outputs
  31. class cython(Task.Task):
  32. run_str = '${CYTHON} ${CYTHONFLAGS} -o ${TGT[0].abspath()} ${SRC}'
  33. color = 'GREEN'
  34. vars = ['INCLUDES']
  35. """
  36. Rebuild whenever the INCLUDES change. The variables such as CYTHONFLAGS will be appended
  37. by the metaclass.
  38. """
  39. ext_out = ['.h']
  40. """
  41. The creation of a .h file is known only after the build has begun, so it is not
  42. possible to compute a build order just by looking at the task inputs/outputs.
  43. """
  44. def runnable_status(self):
  45. """
  46. Perform a double-check to add the headers created by cython
  47. to the output nodes. The scanner is executed only when the cython task
  48. must be executed (optimization).
  49. """
  50. ret = super(cython, self).runnable_status()
  51. if ret == Task.ASK_LATER:
  52. return ret
  53. for x in self.generator.bld.raw_deps[self.uid()]:
  54. if x.startswith('header:'):
  55. self.outputs.append(self.inputs[0].parent.find_or_declare(x.replace('header:', '')))
  56. return super(cython, self).runnable_status()
  57. def post_run(self):
  58. for x in self.outputs:
  59. if x.name.endswith('.h'):
  60. if not x.exists():
  61. if Logs.verbose:
  62. Logs.warn('Expected %r', x.abspath())
  63. x.write('')
  64. return Task.Task.post_run(self)
  65. def scan(self):
  66. """
  67. Return the dependent files (.pxd) by looking in the include folders.
  68. Put the headers to generate in the custom list "bld.raw_deps".
  69. To inspect the scanne results use::
  70. $ waf clean build --zones=deps
  71. """
  72. node = self.inputs[0]
  73. txt = node.read()
  74. mods = []
  75. for m in re_cyt.finditer(txt):
  76. if m.group(1): # matches "from foo import bar"
  77. mods.append(m.group(1))
  78. else:
  79. mods.append(m.group(2))
  80. Logs.debug('cython: mods %r', mods)
  81. incs = getattr(self.generator, 'cython_includes', [])
  82. incs = [self.generator.path.find_dir(x) for x in incs]
  83. incs.append(node.parent)
  84. found = []
  85. missing = []
  86. for x in mods:
  87. for y in incs:
  88. k = y.find_resource(x + '.pxd')
  89. if k:
  90. found.append(k)
  91. break
  92. else:
  93. missing.append(x)
  94. # the cython file implicitly depends on a pxd file that might be present
  95. implicit = node.parent.find_resource(node.name[:-3] + 'pxd')
  96. if implicit:
  97. found.append(implicit)
  98. Logs.debug('cython: found %r', found)
  99. # Now the .h created - store them in bld.raw_deps for later use
  100. has_api = False
  101. has_public = False
  102. for l in txt.splitlines():
  103. if cy_api_pat.match(l):
  104. if ' api ' in l:
  105. has_api = True
  106. if ' public ' in l:
  107. has_public = True
  108. name = node.name.replace('.pyx', '')
  109. if has_api:
  110. missing.append('header:%s_api.h' % name)
  111. if has_public:
  112. missing.append('header:%s.h' % name)
  113. return (found, missing)
  114. def options(ctx):
  115. ctx.add_option('--cython-flags', action='store', default='', help='space separated list of flags to pass to cython')
  116. def configure(ctx):
  117. if not ctx.env.CC and not ctx.env.CXX:
  118. ctx.fatal('Load a C/C++ compiler first')
  119. if not ctx.env.PYTHON:
  120. ctx.fatal('Load the python tool first!')
  121. ctx.find_program('cython', var='CYTHON')
  122. if hasattr(ctx.options, 'cython_flags'):
  123. ctx.env.CYTHONFLAGS = ctx.options.cython_flags