aboutsummaryrefslogtreecommitdiffstats
path: root/waflib/extras/cython.py
blob: 481d6f4c34a9d3ef94abd74953f2b1d0bea56b08 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#! /usr/bin/env python
# encoding: utf-8
# Thomas Nagy, 2010-2015

import re
from waflib import Task, Logs
from waflib.TaskGen import extension

cy_api_pat = re.compile(r'\s*?cdef\s*?(public|api)\w*')
re_cyt = re.compile(r"""
	(?:from\s+(\w+)\s+)?   # optionally match "from foo" and capture foo
	c?import\s(\w+|[*])    # require "import bar" and capture bar
	""", re.M | re.VERBOSE)

@extension('.pyx')
def add_cython_file(self, node):
	"""
	Process a *.pyx* file given in the list of source files. No additional
	feature is required::

		def build(bld):
			bld(features='c cshlib pyext', source='main.c foo.pyx', target='app')
	"""
	ext = '.c'
	if 'cxx' in self.features:
		self.env.append_unique('CYTHONFLAGS', '--cplus')
		ext = '.cc'

	for x in getattr(self, 'cython_includes', []):
		# TODO re-use these nodes in "scan" below
		d = self.path.find_dir(x)
		if d:
			self.env.append_unique('CYTHONFLAGS', '-I%s' % d.abspath())

	tsk = self.create_task('cython', node, node.change_ext(ext))
	self.source += tsk.outputs

class cython(Task.Task):
	run_str = '${CYTHON} ${CYTHONFLAGS} -o ${TGT[0].abspath()} ${SRC}'
	color   = 'GREEN'

	vars    = ['INCLUDES']
	"""
	Rebuild whenever the INCLUDES change. The variables such as CYTHONFLAGS will be appended
	by the metaclass.
	"""

	ext_out = ['.h']
	"""
	The creation of a .h file is known only after the build has begun, so it is not
	possible to compute a build order just by looking at the task inputs/outputs.
	"""

	def runnable_status(self):
		"""
		Perform a double-check to add the headers created by cython
		to the output nodes. The scanner is executed only when the cython task
		must be executed (optimization).
		"""
		ret = super(cython, self).runnable_status()
		if ret == Task.ASK_LATER:
			return ret
		for x in self.generator.bld.raw_deps[self.uid()]:
			if x.startswith('header:'):
				self.outputs.append(self.inputs[0].parent.find_or_declare(x.replace('header:', '')))
		return super(cython, self).runnable_status()

	def post_run(self):
		for x in self.outputs:
			if x.name.endswith('.h'):
				if not x.exists():
					if Logs.verbose:
						Logs.warn('Expected %r', x.abspath())
					x.write('')
		return Task.Task.post_run(self)

	def scan(self):
		"""
		Return the dependent files (.pxd) by looking in the include folders.
		Put the headers to generate in the custom list "bld.raw_deps".
		To inspect the scanne results use::

			$ waf clean build --zones=deps
		"""
		node = self.inputs[0]
		txt = node.read()

		mods = []
		for m in re_cyt.finditer(txt):
			if m.group(1):  # matches "from foo import bar"
				mods.append(m.group(1))
			else:
				mods.append(m.group(2))

		Logs.debug('cython: mods %r', mods)
		incs = getattr(self.generator, 'cython_includes', [])
		incs = [self.generator.path.find_dir(x) for x in incs]
		incs.append(node.parent)

		found = []
		missing = []
		for x in mods:
			for y in incs:
				k = y.find_resource(x + '.pxd')
				if k:
					found.append(k)
					break
			else:
				missing.append(x)

		# the cython file implicitly depends on a pxd file that might be present
		implicit = node.parent.find_resource(node.name[:-3] + 'pxd')
		if implicit:
			found.append(implicit)

		Logs.debug('cython: found %r', found)

		# Now the .h created - store them in bld.raw_deps for later use
		has_api = False
		has_public = False
		for l in txt.splitlines():
			if cy_api_pat.match(l):
				if ' api ' in l:
					has_api = True
				if ' public ' in l:
					has_public = True
		name = node.name.replace('.pyx', '')
		if has_api:
			missing.append('header:%s_api.h' % name)
		if has_public:
			missing.append('header:%s.h' % name)

		return (found, missing)

def options(ctx):
	ctx.add_option('--cython-flags', action='store', default='', help='space separated list of flags to pass to cython')

def configure(ctx):
	if not ctx.env.CC and not ctx.env.CXX:
		ctx.fatal('Load a C/C++ compiler first')
	if not ctx.env.PYTHON:
		ctx.fatal('Load the python tool first!')
	ctx.find_program('cython', var='CYTHON')
	if hasattr(ctx.options, 'cython_flags'):
		ctx.env.CYTHONFLAGS = ctx.options.cython_flags