1#! /usr/bin/env python
2# encoding: utf-8
3# DC 2008
4# Thomas Nagy 2016-2018 (ita)
5
6"""
7Fortran configuration helpers
8"""
9
10import re, os, sys, shlex
11from waflib.Configure import conf
12from waflib.TaskGen import feature, before_method
13
14FC_FRAGMENT = '        program main\n        end     program main\n'
15FC_FRAGMENT2 = '        PROGRAM MAIN\n        END\n' # what's the actual difference between these?
16
17@conf
18def fc_flags(conf):
19	"""
20	Defines common fortran configuration flags and file extensions
21	"""
22	v = conf.env
23
24	v.FC_SRC_F    = []
25	v.FC_TGT_F    = ['-c', '-o']
26	v.FCINCPATH_ST  = '-I%s'
27	v.FCDEFINES_ST  = '-D%s'
28
29	if not v.LINK_FC:
30		v.LINK_FC = v.FC
31
32	v.FCLNK_SRC_F = []
33	v.FCLNK_TGT_F = ['-o']
34
35	v.FCFLAGS_fcshlib   = ['-fpic']
36	v.LINKFLAGS_fcshlib = ['-shared']
37	v.fcshlib_PATTERN   = 'lib%s.so'
38
39	v.fcstlib_PATTERN   = 'lib%s.a'
40
41	v.FCLIB_ST       = '-l%s'
42	v.FCLIBPATH_ST   = '-L%s'
43	v.FCSTLIB_ST     = '-l%s'
44	v.FCSTLIBPATH_ST = '-L%s'
45	v.FCSTLIB_MARKER = '-Wl,-Bstatic'
46	v.FCSHLIB_MARKER = '-Wl,-Bdynamic'
47
48	v.SONAME_ST      = '-Wl,-h,%s'
49
50@conf
51def fc_add_flags(conf):
52	"""
53	Adds FCFLAGS / LDFLAGS / LINKFLAGS from os.environ to conf.env
54	"""
55	conf.add_os_flags('FCPPFLAGS', dup=False)
56	conf.add_os_flags('FCFLAGS', dup=False)
57	conf.add_os_flags('LINKFLAGS', dup=False)
58	conf.add_os_flags('LDFLAGS', dup=False)
59
60@conf
61def check_fortran(self, *k, **kw):
62	"""
63	Compiles a Fortran program to ensure that the settings are correct
64	"""
65	self.check_cc(
66		fragment         = FC_FRAGMENT,
67		compile_filename = 'test.f',
68		features         = 'fc fcprogram',
69		msg              = 'Compiling a simple fortran app')
70
71@conf
72def check_fc(self, *k, **kw):
73	"""
74	Same as :py:func:`waflib.Tools.c_config.check` but defaults to the *Fortran* programming language
75	(this overrides the C defaults in :py:func:`waflib.Tools.c_config.validate_c`)
76	"""
77	kw['compiler'] = 'fc'
78	if not 'compile_mode' in kw:
79		kw['compile_mode'] = 'fc'
80	if not 'type' in kw:
81		kw['type'] = 'fcprogram'
82	if not 'compile_filename' in kw:
83		kw['compile_filename'] = 'test.f90'
84	if not 'code' in kw:
85		kw['code'] = FC_FRAGMENT
86	return self.check(*k, **kw)
87
88# ------------------------------------------------------------------------
89# --- These are the default platform modifiers, refactored here for
90#     convenience.  gfortran and g95 have much overlap.
91# ------------------------------------------------------------------------
92
93@conf
94def fortran_modifier_darwin(conf):
95	"""
96	Defines Fortran flags and extensions for OSX systems
97	"""
98	v = conf.env
99	v.FCFLAGS_fcshlib   = ['-fPIC']
100	v.LINKFLAGS_fcshlib = ['-dynamiclib']
101	v.fcshlib_PATTERN   = 'lib%s.dylib'
102	v.FRAMEWORKPATH_ST  = '-F%s'
103	v.FRAMEWORK_ST      = ['-framework']
104
105	v.LINKFLAGS_fcstlib = []
106
107	v.FCSHLIB_MARKER    = ''
108	v.FCSTLIB_MARKER    = ''
109	v.SONAME_ST         = ''
110
111@conf
112def fortran_modifier_win32(conf):
113	"""
114	Defines Fortran flags for Windows platforms
115	"""
116	v = conf.env
117	v.fcprogram_PATTERN = v.fcprogram_test_PATTERN  = '%s.exe'
118
119	v.fcshlib_PATTERN   = '%s.dll'
120	v.implib_PATTERN    = '%s.dll.a'
121	v.IMPLIB_ST         = '-Wl,--out-implib,%s'
122
123	v.FCFLAGS_fcshlib   = []
124
125	# Auto-import is enabled by default even without this option,
126	# but enabling it explicitly has the nice effect of suppressing the rather boring, debug-level messages
127	# that the linker emits otherwise.
128	v.append_value('LINKFLAGS', ['-Wl,--enable-auto-import'])
129
130@conf
131def fortran_modifier_cygwin(conf):
132	"""
133	Defines Fortran flags for use on cygwin
134	"""
135	fortran_modifier_win32(conf)
136	v = conf.env
137	v.fcshlib_PATTERN = 'cyg%s.dll'
138	v.append_value('LINKFLAGS_fcshlib', ['-Wl,--enable-auto-image-base'])
139	v.FCFLAGS_fcshlib = []
140
141# ------------------------------------------------------------------------
142
143@conf
144def check_fortran_dummy_main(self, *k, **kw):
145	"""
146	Determines if a main function is needed by compiling a code snippet with
147	the C compiler and linking it with the Fortran compiler (useful on unix-like systems)
148	"""
149	if not self.env.CC:
150		self.fatal('A c compiler is required for check_fortran_dummy_main')
151
152	lst = ['MAIN__', '__MAIN', '_MAIN', 'MAIN_', 'MAIN']
153	lst.extend([m.lower() for m in lst])
154	lst.append('')
155
156	self.start_msg('Detecting whether we need a dummy main')
157	for main in lst:
158		kw['fortran_main'] = main
159		try:
160			self.check_cc(
161				fragment = 'int %s() { return 0; }\n' % (main or 'test'),
162				features = 'c fcprogram',
163				mandatory = True
164			)
165			if not main:
166				self.env.FC_MAIN = -1
167				self.end_msg('no')
168			else:
169				self.env.FC_MAIN = main
170				self.end_msg('yes %s' % main)
171			break
172		except self.errors.ConfigurationError:
173			pass
174	else:
175		self.end_msg('not found')
176		self.fatal('could not detect whether fortran requires a dummy main, see the config.log')
177
178# ------------------------------------------------------------------------
179
180GCC_DRIVER_LINE = re.compile('^Driving:')
181POSIX_STATIC_EXT = re.compile(r'\S+\.a')
182POSIX_LIB_FLAGS = re.compile(r'-l\S+')
183
184@conf
185def is_link_verbose(self, txt):
186	"""Returns True if 'useful' link options can be found in txt"""
187	assert isinstance(txt, str)
188	for line in txt.splitlines():
189		if not GCC_DRIVER_LINE.search(line):
190			if POSIX_STATIC_EXT.search(line) or POSIX_LIB_FLAGS.search(line):
191				return True
192	return False
193
194@conf
195def check_fortran_verbose_flag(self, *k, **kw):
196	"""
197	Checks what kind of verbose (-v) flag works, then sets it to env.FC_VERBOSE_FLAG
198	"""
199	self.start_msg('fortran link verbose flag')
200	for x in ('-v', '--verbose', '-verbose', '-V'):
201		try:
202			self.check_cc(
203				features = 'fc fcprogram_test',
204				fragment = FC_FRAGMENT2,
205				compile_filename = 'test.f',
206				linkflags = [x],
207				mandatory=True)
208		except self.errors.ConfigurationError:
209			pass
210		else:
211			# output is on stderr or stdout (for xlf)
212			if self.is_link_verbose(self.test_bld.err) or self.is_link_verbose(self.test_bld.out):
213				self.end_msg(x)
214				break
215	else:
216		self.end_msg('failure')
217		self.fatal('Could not obtain the fortran link verbose flag (see config.log)')
218
219	self.env.FC_VERBOSE_FLAG = x
220	return x
221
222# ------------------------------------------------------------------------
223
224# linkflags which match those are ignored
225LINKFLAGS_IGNORED = [r'-lang*', r'-lcrt[a-zA-Z0-9\.]*\.o', r'-lc$', r'-lSystem', r'-libmil', r'-LIST:*', r'-LNO:*']
226if os.name == 'nt':
227	LINKFLAGS_IGNORED.extend([r'-lfrt*', r'-luser32', r'-lkernel32', r'-ladvapi32', r'-lmsvcrt', r'-lshell32', r'-lmingw', r'-lmoldname'])
228else:
229	LINKFLAGS_IGNORED.append(r'-lgcc*')
230RLINKFLAGS_IGNORED = [re.compile(f) for f in LINKFLAGS_IGNORED]
231
232def _match_ignore(line):
233	"""Returns True if the line should be ignored (Fortran verbose flag test)"""
234	for i in RLINKFLAGS_IGNORED:
235		if i.match(line):
236			return True
237	return False
238
239def parse_fortran_link(lines):
240	"""Given the output of verbose link of Fortran compiler, this returns a
241	list of flags necessary for linking using the standard linker."""
242	final_flags = []
243	for line in lines:
244		if not GCC_DRIVER_LINE.match(line):
245			_parse_flink_line(line, final_flags)
246	return final_flags
247
248SPACE_OPTS = re.compile('^-[LRuYz]$')
249NOSPACE_OPTS = re.compile('^-[RL]')
250
251def _parse_flink_token(lexer, token, tmp_flags):
252	# Here we go (convention for wildcard is shell, not regex !)
253	#   1 TODO: we first get some root .a libraries
254	#   2 TODO: take everything starting by -bI:*
255	#   3 Ignore the following flags: -lang* | -lcrt*.o | -lc |
256	#   -lgcc* | -lSystem | -libmil | -LANG:=* | -LIST:* | -LNO:*)
257	#   4 take into account -lkernel32
258	#   5 For options of the kind -[[LRuYz]], as they take one argument
259	#   after, the actual option is the next token
260	#   6 For -YP,*: take and replace by -Larg where arg is the old
261	#   argument
262	#   7 For -[lLR]*: take
263
264	# step 3
265	if _match_ignore(token):
266		pass
267	# step 4
268	elif token.startswith('-lkernel32') and sys.platform == 'cygwin':
269		tmp_flags.append(token)
270	# step 5
271	elif SPACE_OPTS.match(token):
272		t = lexer.get_token()
273		if t.startswith('P,'):
274			t = t[2:]
275		for opt in t.split(os.pathsep):
276			tmp_flags.append('-L%s' % opt)
277	# step 6
278	elif NOSPACE_OPTS.match(token):
279		tmp_flags.append(token)
280	# step 7
281	elif POSIX_LIB_FLAGS.match(token):
282		tmp_flags.append(token)
283	else:
284		# ignore anything not explicitly taken into account
285		pass
286
287	t = lexer.get_token()
288	return t
289
290def _parse_flink_line(line, final_flags):
291	"""private"""
292	lexer = shlex.shlex(line, posix = True)
293	lexer.whitespace_split = True
294
295	t = lexer.get_token()
296	tmp_flags = []
297	while t:
298		t = _parse_flink_token(lexer, t, tmp_flags)
299
300	final_flags.extend(tmp_flags)
301	return final_flags
302
303@conf
304def check_fortran_clib(self, autoadd=True, *k, **kw):
305	"""
306	Obtains the flags for linking with the C library
307	if this check works, add uselib='CLIB' to your task generators
308	"""
309	if not self.env.FC_VERBOSE_FLAG:
310		self.fatal('env.FC_VERBOSE_FLAG is not set: execute check_fortran_verbose_flag?')
311
312	self.start_msg('Getting fortran runtime link flags')
313	try:
314		self.check_cc(
315			fragment = FC_FRAGMENT2,
316			compile_filename = 'test.f',
317			features = 'fc fcprogram_test',
318			linkflags = [self.env.FC_VERBOSE_FLAG]
319		)
320	except Exception:
321		self.end_msg(False)
322		if kw.get('mandatory', True):
323			conf.fatal('Could not find the c library flags')
324	else:
325		out = self.test_bld.err
326		flags = parse_fortran_link(out.splitlines())
327		self.end_msg('ok (%s)' % ' '.join(flags))
328		self.env.LINKFLAGS_CLIB = flags
329		return flags
330	return []
331
332def getoutput(conf, cmd, stdin=False):
333	"""
334	Obtains Fortran command outputs
335	"""
336	from waflib import Errors
337	if conf.env.env:
338		env = conf.env.env
339	else:
340		env = dict(os.environ)
341		env['LANG'] = 'C'
342	input = stdin and '\n'.encode() or None
343	try:
344		out, err = conf.cmd_and_log(cmd, env=env, output=0, input=input)
345	except Errors.WafError as e:
346		# An WafError might indicate an error code during the command
347		# execution, in this case we still obtain the stderr and stdout,
348		# which we can use to find the version string.
349		if not (hasattr(e, 'stderr') and hasattr(e, 'stdout')):
350			raise e
351		else:
352			# Ignore the return code and return the original
353			# stdout and stderr.
354			out = e.stdout
355			err = e.stderr
356	except Exception:
357		conf.fatal('could not determine the compiler version %r' % cmd)
358	return (out, err)
359
360# ------------------------------------------------------------------------
361
362ROUTINES_CODE = """\
363      subroutine foobar()
364      return
365      end
366      subroutine foo_bar()
367      return
368      end
369"""
370
371MAIN_CODE = """
372void %(dummy_func_nounder)s(void);
373void %(dummy_func_under)s(void);
374int %(main_func_name)s() {
375  %(dummy_func_nounder)s();
376  %(dummy_func_under)s();
377  return 0;
378}
379"""
380
381@feature('link_main_routines_func')
382@before_method('process_source')
383def link_main_routines_tg_method(self):
384	"""
385	The configuration test declares a unique task generator,
386	so we create other task generators from there for fortran link tests
387	"""
388	def write_test_file(task):
389		task.outputs[0].write(task.generator.code)
390	bld = self.bld
391	bld(rule=write_test_file, target='main.c', code=MAIN_CODE % self.__dict__)
392	bld(rule=write_test_file, target='test.f', code=ROUTINES_CODE)
393	bld(features='fc fcstlib', source='test.f', target='test')
394	bld(features='c fcprogram', source='main.c', target='app', use='test')
395
396def mangling_schemes():
397	"""
398	Generate triplets for use with mangle_name
399	(used in check_fortran_mangling)
400	the order is tuned for gfortan
401	"""
402	for u in ('_', ''):
403		for du in ('', '_'):
404			for c in ("lower", "upper"):
405				yield (u, du, c)
406
407def mangle_name(u, du, c, name):
408	"""Mangle a name from a triplet (used in check_fortran_mangling)"""
409	return getattr(name, c)() + u + (name.find('_') != -1 and du or '')
410
411@conf
412def check_fortran_mangling(self, *k, **kw):
413	"""
414	Detect the mangling scheme, sets FORTRAN_MANGLING to the triplet found
415
416	This test will compile a fortran static library, then link a c app against it
417	"""
418	if not self.env.CC:
419		self.fatal('A c compiler is required for link_main_routines')
420	if not self.env.FC:
421		self.fatal('A fortran compiler is required for link_main_routines')
422	if not self.env.FC_MAIN:
423		self.fatal('Checking for mangling requires self.env.FC_MAIN (execute "check_fortran_dummy_main" first?)')
424
425	self.start_msg('Getting fortran mangling scheme')
426	for (u, du, c) in mangling_schemes():
427		try:
428			self.check_cc(
429				compile_filename   = [],
430				features           = 'link_main_routines_func',
431				msg                = 'nomsg',
432				errmsg             = 'nomsg',
433				dummy_func_nounder = mangle_name(u, du, c, 'foobar'),
434				dummy_func_under   = mangle_name(u, du, c, 'foo_bar'),
435				main_func_name     = self.env.FC_MAIN
436			)
437		except self.errors.ConfigurationError:
438			pass
439		else:
440			self.end_msg("ok ('%s', '%s', '%s-case')" % (u, du, c))
441			self.env.FORTRAN_MANGLING = (u, du, c)
442			break
443	else:
444		self.end_msg(False)
445		self.fatal('mangler not found')
446	return (u, du, c)
447
448@feature('pyext')
449@before_method('propagate_uselib_vars', 'apply_link')
450def set_lib_pat(self):
451	"""Sets the Fortran flags for linking with Python"""
452	self.env.fcshlib_PATTERN = self.env.pyext_PATTERN
453
454@conf
455def detect_openmp(self):
456	"""
457	Detects openmp flags and sets the OPENMP ``FCFLAGS``/``LINKFLAGS``
458	"""
459	for x in ('-fopenmp','-openmp','-mp','-xopenmp','-omp','-qsmp=omp'):
460		try:
461			self.check_fc(
462				msg          = 'Checking for OpenMP flag %s' % x,
463				fragment     = 'program main\n  call omp_get_num_threads()\nend program main',
464				fcflags      = x,
465				linkflags    = x,
466				uselib_store = 'OPENMP'
467			)
468		except self.errors.ConfigurationError:
469			pass
470		else:
471			break
472	else:
473		self.fatal('Could not find OpenMP')
474
475@conf
476def check_gfortran_o_space(self):
477	if self.env.FC_NAME != 'GFORTRAN' or int(self.env.FC_VERSION[0]) > 4:
478		# This is for old compilers and only for gfortran.
479		# No idea how other implementations handle this. Be safe and bail out.
480		return
481	self.env.stash()
482	self.env.FCLNK_TGT_F = ['-o', '']
483	try:
484		self.check_fc(msg='Checking if the -o link must be split from arguments', fragment=FC_FRAGMENT, features='fc fcshlib')
485	except self.errors.ConfigurationError:
486		self.env.revert()
487	else:
488		self.env.commit()
489