1#!/usr/bin/env python
2# encoding: utf-8
3# Thomas Nagy, 2006-2010 (ita)
4
5"ocaml support"
6
7import os, re
8from waflib import Utils, Task
9from waflib.Logs import error
10from waflib.TaskGen import feature, before_method, after_method, extension
11
12EXT_MLL = ['.mll']
13EXT_MLY = ['.mly']
14EXT_MLI = ['.mli']
15EXT_MLC = ['.c']
16EXT_ML  = ['.ml']
17
18open_re = re.compile(r'^\s*open\s+([a-zA-Z]+)(;;){0,1}$', re.M)
19foo = re.compile(r"""(\(\*)|(\*\))|("(\\.|[^"\\])*"|'(\\.|[^'\\])*'|.[^()*"'\\]*)""", re.M)
20def filter_comments(txt):
21	meh = [0]
22	def repl(m):
23		if m.group(1):
24			meh[0] += 1
25		elif m.group(2):
26			meh[0] -= 1
27		elif not meh[0]:
28			return m.group()
29		return ''
30	return foo.sub(repl, txt)
31
32def scan(self):
33	node = self.inputs[0]
34	code = filter_comments(node.read())
35
36	global open_re
37	names = []
38	import_iterator = open_re.finditer(code)
39	if import_iterator:
40		for import_match in import_iterator:
41			names.append(import_match.group(1))
42	found_lst = []
43	raw_lst = []
44	for name in names:
45		nd = None
46		for x in self.incpaths:
47			nd = x.find_resource(name.lower()+'.ml')
48			if not nd:
49				nd = x.find_resource(name+'.ml')
50			if nd:
51				found_lst.append(nd)
52				break
53		else:
54			raw_lst.append(name)
55
56	return (found_lst, raw_lst)
57
58native_lst=['native', 'all', 'c_object']
59bytecode_lst=['bytecode', 'all']
60
61@feature('ocaml')
62def init_ml(self):
63	Utils.def_attrs(self,
64		type = 'all',
65		incpaths_lst = [],
66		bld_incpaths_lst = [],
67		mlltasks = [],
68		mlytasks = [],
69		mlitasks = [],
70		native_tasks = [],
71		bytecode_tasks = [],
72		linktasks = [],
73		bytecode_env = None,
74		native_env = None,
75		compiled_tasks = [],
76		includes = '',
77		uselib = '',
78		are_deps_set = 0)
79
80@feature('ocaml')
81@after_method('init_ml')
82def init_envs_ml(self):
83
84	self.islibrary = getattr(self, 'islibrary', False)
85
86	global native_lst, bytecode_lst
87	self.native_env = None
88	if self.type in native_lst:
89		self.native_env = self.env.derive()
90		if self.islibrary:
91			self.native_env['OCALINKFLAGS']   = '-a'
92
93	self.bytecode_env = None
94	if self.type in bytecode_lst:
95		self.bytecode_env = self.env.derive()
96		if self.islibrary:
97			self.bytecode_env['OCALINKFLAGS'] = '-a'
98
99	if self.type == 'c_object':
100		self.native_env.append_unique('OCALINKFLAGS_OPT', '-output-obj')
101
102@feature('ocaml')
103@before_method('apply_vars_ml')
104@after_method('init_envs_ml')
105def apply_incpaths_ml(self):
106	inc_lst = self.includes.split()
107	lst = self.incpaths_lst
108	for dir in inc_lst:
109		node = self.path.find_dir(dir)
110		if not node:
111			error("node not found: " + str(dir))
112			continue
113		if not node in lst:
114			lst.append(node)
115		self.bld_incpaths_lst.append(node)
116	# now the nodes are added to self.incpaths_lst
117
118@feature('ocaml')
119@before_method('process_source')
120def apply_vars_ml(self):
121	for i in self.incpaths_lst:
122		if self.bytecode_env:
123			app = self.bytecode_env.append_value
124			app('OCAMLPATH', ['-I', i.bldpath(), '-I', i.srcpath()])
125
126		if self.native_env:
127			app = self.native_env.append_value
128			app('OCAMLPATH', ['-I', i.bldpath(), '-I', i.srcpath()])
129
130	varnames = ['INCLUDES', 'OCAMLFLAGS', 'OCALINKFLAGS', 'OCALINKFLAGS_OPT']
131	for name in self.uselib.split():
132		for vname in varnames:
133			cnt = self.env[vname+'_'+name]
134			if cnt:
135				if self.bytecode_env:
136					self.bytecode_env.append_value(vname, cnt)
137				if self.native_env:
138					self.native_env.append_value(vname, cnt)
139
140@feature('ocaml')
141@after_method('process_source')
142def apply_link_ml(self):
143
144	if self.bytecode_env:
145		ext = self.islibrary and '.cma' or '.run'
146
147		linktask = self.create_task('ocalink')
148		linktask.bytecode = 1
149		linktask.set_outputs(self.path.find_or_declare(self.target + ext))
150		linktask.env = self.bytecode_env
151		self.linktasks.append(linktask)
152
153	if self.native_env:
154		if self.type == 'c_object':
155			ext = '.o'
156		elif self.islibrary:
157			ext = '.cmxa'
158		else:
159			ext = ''
160
161		linktask = self.create_task('ocalinkx')
162		linktask.set_outputs(self.path.find_or_declare(self.target + ext))
163		linktask.env = self.native_env
164		self.linktasks.append(linktask)
165
166		# we produce a .o file to be used by gcc
167		self.compiled_tasks.append(linktask)
168
169@extension(*EXT_MLL)
170def mll_hook(self, node):
171	mll_task = self.create_task('ocamllex', node, node.change_ext('.ml'))
172	mll_task.env = self.native_env.derive()
173	self.mlltasks.append(mll_task)
174
175	self.source.append(mll_task.outputs[0])
176
177@extension(*EXT_MLY)
178def mly_hook(self, node):
179	mly_task = self.create_task('ocamlyacc', node, [node.change_ext('.ml'), node.change_ext('.mli')])
180	mly_task.env = self.native_env.derive()
181	self.mlytasks.append(mly_task)
182	self.source.append(mly_task.outputs[0])
183
184	task = self.create_task('ocamlcmi', mly_task.outputs[1], mly_task.outputs[1].change_ext('.cmi'))
185	task.env = self.native_env.derive()
186
187@extension(*EXT_MLI)
188def mli_hook(self, node):
189	task = self.create_task('ocamlcmi', node, node.change_ext('.cmi'))
190	task.env = self.native_env.derive()
191	self.mlitasks.append(task)
192
193@extension(*EXT_MLC)
194def mlc_hook(self, node):
195	task = self.create_task('ocamlcc', node, node.change_ext('.o'))
196	task.env = self.native_env.derive()
197	self.compiled_tasks.append(task)
198
199@extension(*EXT_ML)
200def ml_hook(self, node):
201	if self.native_env:
202		task = self.create_task('ocamlx', node, node.change_ext('.cmx'))
203		task.env = self.native_env.derive()
204		task.incpaths = self.bld_incpaths_lst
205		self.native_tasks.append(task)
206
207	if self.bytecode_env:
208		task = self.create_task('ocaml', node, node.change_ext('.cmo'))
209		task.env = self.bytecode_env.derive()
210		task.bytecode = 1
211		task.incpaths = self.bld_incpaths_lst
212		self.bytecode_tasks.append(task)
213
214def compile_may_start(self):
215
216	if not getattr(self, 'flag_deps', ''):
217		self.flag_deps = 1
218
219		# the evil part is that we can only compute the dependencies after the
220		# source files can be read (this means actually producing the source files)
221		if getattr(self, 'bytecode', ''):
222			alltasks = self.generator.bytecode_tasks
223		else:
224			alltasks = self.generator.native_tasks
225
226		self.signature() # ensure that files are scanned - unfortunately
227		tree = self.generator.bld
228		for node in self.inputs:
229			lst = tree.node_deps[self.uid()]
230			for depnode in lst:
231				for t in alltasks:
232					if t == self:
233						continue
234					if depnode in t.inputs:
235						self.set_run_after(t)
236
237		# TODO necessary to get the signature right - for now
238		delattr(self, 'cache_sig')
239		self.signature()
240
241	return Task.Task.runnable_status(self)
242
243class ocamlx(Task.Task):
244	"""native caml compilation"""
245	color   = 'GREEN'
246	run_str = '${OCAMLOPT} ${OCAMLPATH} ${OCAMLFLAGS} ${OCAMLINCLUDES} -c -o ${TGT} ${SRC}'
247	scan    = scan
248	runnable_status = compile_may_start
249
250class ocaml(Task.Task):
251	"""bytecode caml compilation"""
252	color   = 'GREEN'
253	run_str = '${OCAMLC} ${OCAMLPATH} ${OCAMLFLAGS} ${OCAMLINCLUDES} -c -o ${TGT} ${SRC}'
254	scan    = scan
255	runnable_status = compile_may_start
256
257class ocamlcmi(Task.Task):
258	"""interface generator (the .i files?)"""
259	color   = 'BLUE'
260	run_str = '${OCAMLC} ${OCAMLPATH} ${OCAMLINCLUDES} -o ${TGT} -c ${SRC}'
261	before  = ['ocamlcc', 'ocaml', 'ocamlcc']
262
263class ocamlcc(Task.Task):
264	"""ocaml to c interfaces"""
265	color   = 'GREEN'
266	run_str = 'cd ${TGT[0].bld_dir()} && ${OCAMLOPT} ${OCAMLFLAGS} ${OCAMLPATH} ${OCAMLINCLUDES} -c ${SRC[0].abspath()}'
267
268class ocamllex(Task.Task):
269	"""lexical generator"""
270	color   = 'BLUE'
271	run_str = '${OCAMLLEX} ${SRC} -o ${TGT}'
272	before  = ['ocamlcmi', 'ocaml', 'ocamlcc']
273
274class ocamlyacc(Task.Task):
275	"""parser generator"""
276	color   = 'BLUE'
277	run_str = '${OCAMLYACC} -b ${tsk.base()} ${SRC}'
278	before  = ['ocamlcmi', 'ocaml', 'ocamlcc']
279
280	def base(self):
281		node = self.outputs[0]
282		s = os.path.splitext(node.name)[0]
283		return node.bld_dir() + os.sep + s
284
285def link_may_start(self):
286
287	if getattr(self, 'bytecode', 0):
288		alltasks = self.generator.bytecode_tasks
289	else:
290		alltasks = self.generator.native_tasks
291
292	for x in alltasks:
293		if not x.hasrun:
294			return Task.ASK_LATER
295
296	if not getattr(self, 'order', ''):
297
298		# now reorder the inputs given the task dependencies
299		# this part is difficult, we do not have a total order on the tasks
300		# if the dependencies are wrong, this may not stop
301		seen = []
302		pendant = []+alltasks
303		while pendant:
304			task = pendant.pop(0)
305			if task in seen:
306				continue
307			for x in task.run_after:
308				if not x in seen:
309					pendant.append(task)
310					break
311			else:
312				seen.append(task)
313		self.inputs = [x.outputs[0] for x in seen]
314		self.order = 1
315	return Task.Task.runnable_status(self)
316
317class ocalink(Task.Task):
318	"""bytecode caml link"""
319	color   = 'YELLOW'
320	run_str = '${OCAMLC} -o ${TGT} ${OCAMLINCLUDES} ${OCALINKFLAGS} ${SRC}'
321	runnable_status = link_may_start
322	after = ['ocaml', 'ocamlcc']
323
324class ocalinkx(Task.Task):
325	"""native caml link"""
326	color   = 'YELLOW'
327	run_str = '${OCAMLOPT} -o ${TGT} ${OCAMLINCLUDES} ${OCALINKFLAGS_OPT} ${SRC}'
328	runnable_status = link_may_start
329	after = ['ocamlx', 'ocamlcc']
330
331def configure(conf):
332	opt = conf.find_program('ocamlopt', var='OCAMLOPT', mandatory=False)
333	occ = conf.find_program('ocamlc', var='OCAMLC', mandatory=False)
334	if (not opt) or (not occ):
335		conf.fatal('The objective caml compiler was not found:\ninstall it or make it available in your PATH')
336
337	v = conf.env
338	v['OCAMLC']       = occ
339	v['OCAMLOPT']     = opt
340	v['OCAMLLEX']     = conf.find_program('ocamllex', var='OCAMLLEX', mandatory=False)
341	v['OCAMLYACC']    = conf.find_program('ocamlyacc', var='OCAMLYACC', mandatory=False)
342	v['OCAMLFLAGS']   = ''
343	where = conf.cmd_and_log(conf.env.OCAMLC + ['-where']).strip()+os.sep
344	v['OCAMLLIB']     = where
345	v['LIBPATH_OCAML'] = where
346	v['INCLUDES_OCAML'] = where
347	v['LIB_OCAML'] = 'camlrun'
348
349