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