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