55# granted to it by virtue of its status as an intergovernmental organisation
66# nor does it submit to any jurisdiction.
77
8- from pathlib import Path
98from importlib import import_module , reload
9+ import os
10+ import re
1011import sys
12+ from pathlib import Path
1113
12- from loki .logging import info
13- from loki .tools import execute , as_tuple , flatten , delete
14+ from loki .logging import info , debug
15+ from loki .tools import execute , as_tuple , delete
1416
1517
16- __all__ = ['clean' , 'compile' , 'compile_and_load' ,
17- '_default_compiler' , 'Compiler' , 'GNUCompiler' , 'EscapeGNUCompiler' ]
18+ __all__ = [
19+ 'clean' , 'compile' , 'compile_and_load' , '_default_compiler' ,
20+ 'Compiler' , 'get_compiler_from_env' , 'GNUCompiler' , 'NvidiaCompiler'
21+ ]
1822
1923
2024def compile (filename , include_dirs = None , compiler = None , cwd = None ):
@@ -41,7 +45,7 @@ def clean(filename, pattern=None):
4145 delete (f )
4246
4347
44- def compile_and_load (filename , cwd = None , use_f90wrap = True , f90wrap_kind_map = None ): # pylint: disable=unused-argument
48+ def compile_and_load (filename , cwd = None , f90wrap_kind_map = None , compiler = None ):
4549 """
4650 Just-in-time compile Fortran source code and load the respective
4751 module or class.
@@ -50,17 +54,17 @@ def compile_and_load(filename, cwd=None, use_f90wrap=True, f90wrap_kind_map=None
5054 supported via the ``f2py`` and ``f90wrap`` packages.
5155
5256 Parameters
53- -----
57+ ----------
5458 filename : str
5559 The source file to be compiled.
5660 cwd : str, optional
5761 Working directory to use for calls to compiler.
58- use_f90wrap : bool, optional
59- Flag to trigger the ``f90wrap`` compiler required
60- if the source code includes module or derived types.
6162 f90wrap_kind_map : str, optional
6263 Path to ``f90wrap`` KIND_MAP file, containing a Python dictionary
6364 in f2py_f2cmap format.
65+ compiler : :any:`Compiler`, optional
66+ Use the specified compiler to compile the Fortran source code. Defaults
67+ to :any:`_default_compiler`
6468 """
6569 info (f'Compiling: { filename } ' )
6670 filepath = Path (filename )
@@ -71,25 +75,19 @@ def compile_and_load(filename, cwd=None, use_f90wrap=True, f90wrap_kind_map=None
7175 clean (filename , pattern = pattern )
7276
7377 # First, compile the module and object files
74- build = ['gfortran' , '-c' , '-fpic' , str (filepath .absolute ())]
75- execute (build , cwd = cwd )
78+ if not compiler :
79+ compiler = _default_compiler
80+ compiler .compile (filepath .absolute (), cwd = cwd )
7681
7782 # Generate the Python interfaces
78- f90wrap = ['f90wrap' ]
79- f90wrap += ['-m' , str (filepath .stem )]
80- if f90wrap_kind_map is not None :
81- f90wrap += ['-k' , str (f90wrap_kind_map )]
82- f90wrap += [str (filepath .absolute ())]
83- execute (f90wrap , cwd = cwd )
83+ compiler .f90wrap (modname = filepath .stem , source = [filepath .absolute ()], kind_map = f90wrap_kind_map , cwd = cwd )
8484
8585 # Compile the dynamic library
86- f2py = ['f2py-f90wrap' , '-c' ]
87- f2py += ['-m' , f'_{ filepath .stem } ' ]
88- f2py += [f'{ filepath .stem } .o' ]
86+ f2py_source = [f'{ filepath .stem } .o' ]
8987 for sourcefile in [f'f90wrap_{ filepath .stem } .f90' , 'f90wrap_toplevel.f90' ]:
9088 if (filepath .parent / sourcefile ).exists ():
91- f2py += [sourcefile ]
92- execute ( f2py , cwd = cwd )
89+ f2py_source += [sourcefile ]
90+ compiler . f2py ( modname = filepath . stem , source = f2py_source , cwd = cwd )
9391
9492 # Add directory to module search path
9593 moddir = str (filepath .parent )
@@ -120,6 +118,7 @@ class Compiler:
120118 LDFLAGS = None
121119 LD_STATIC = None
122120 LDFLAGS_STATIC = None
121+ F2PY_FCOMPILER_TYPE = None
123122
124123 def __init__ (self ):
125124 self .cc = self .CC or 'gcc'
@@ -132,24 +131,55 @@ def __init__(self):
132131 self .ldflags = self .LDFLAGS or ['-static' ]
133132 self .ld_static = self .LD_STATIC or 'ar'
134133 self .ldflags_static = self .LDFLAGS_STATIC or ['src' ]
134+ self .f2py_fcompiler_type = self .F2PY_FCOMPILER_TYPE or 'gnu95'
135135
136- def compile_args (self , source , target = None , include_dirs = None , mod_dir = None , mode = 'F90 ' ):
136+ def compile_args (self , source , target = None , include_dirs = None , mod_dir = None , mode = 'f90 ' ):
137137 """
138138 Generate arguments for the build line.
139139
140- :param mode: One of ``'f90'`` (free form), ``'f'`` (fixed form) or ``'c'``.
140+ Parameters:
141+ -----------
142+ source : str or pathlib.Path
143+ Path to the source file to compile
144+ target : str or pathlib.Path, optional
145+ Path to the output binary to generate
146+ include_dirs : list of str or pathlib.Path, optional
147+ Path of include directories to specify during compile
148+ mod_dir : str or pathlib.Path, optional
149+ Path to directory containing Fortran .mod files
150+ mode : str, optional
151+ One of ``'f90'`` (free form), ``'f'`` (fixed form) or ``'c'``
141152 """
142153 assert mode in ['f90' , 'f' , 'c' ]
143154 include_dirs = include_dirs or []
144155 cc = {'f90' : self .f90 , 'f' : self .fc , 'c' : self .cc }[mode ]
145156 args = [cc , '-c' ]
146157 args += {'f90' : self .f90flags , 'f' : self .fcflags , 'c' : self .cflags }[mode ]
147- args += flatten ([('-I' , str (incl )) for incl in include_dirs ])
148- args += [] if mod_dir is None else ['-J' , str (mod_dir )]
158+ args += self ._include_dir_args (include_dirs )
159+ if mode != 'c' :
160+ args += self ._mod_dir_args (mod_dir )
149161 args += [] if target is None else ['-o' , str (target )]
150162 args += [str (source )]
151163 return args
152164
165+ def _include_dir_args (self , include_dirs ):
166+ """
167+ Return a list of compile command arguments for adding
168+ all paths in :data:`include_dirs` as include directories
169+ """
170+ return [
171+ f'-I{ incl !s} ' for incl in as_tuple (include_dirs )
172+ ]
173+
174+ def _mod_dir_args (self , mod_dir ):
175+ """
176+ Return a list of compile command arguments for setting
177+ :data:`mod_dir` as search and output directory for module files
178+ """
179+ if mod_dir is None :
180+ return []
181+ return [f'-J{ mod_dir !s} ' ]
182+
153183 def compile (self , source , target = None , include_dirs = None , use_c = False , cwd = None ):
154184 """
155185 Execute a build command for a given source.
@@ -200,8 +230,7 @@ def f90wrap(self, modname, source, cwd=None, kind_map=None):
200230 args = self .f90wrap_args (modname = modname , source = source , kind_map = kind_map )
201231 execute (args , cwd = cwd )
202232
203- @staticmethod
204- def f2py_args (modname , source , libs = None , lib_dirs = None , incl_dirs = None ):
233+ def f2py_args (self , modname , source , libs = None , lib_dirs = None , incl_dirs = None ):
205234 """
206235 Generate arguments for the ``f2py-f90wrap`` utility invocation line.
207236 """
@@ -210,6 +239,9 @@ def f2py_args(modname, source, libs=None, lib_dirs=None, incl_dirs=None):
210239 incl_dirs = incl_dirs or []
211240
212241 args = ['f2py-f90wrap' , '-c' ]
242+ args += [f'--fcompiler={ self .f2py_fcompiler_type } ' ]
243+ args += [f'--f77exec={ self .fc } ' ]
244+ args += [f'--f90exec={ self .f90 } ' ]
213245 args += ['-m' , f'_{ modname } ' ]
214246 for incl_dir in incl_dirs :
215247 args += [f'-I{ incl_dir } ' ]
@@ -229,27 +261,129 @@ def f2py(self, modname, source, libs=None, lib_dirs=None, incl_dirs=None, cwd=No
229261 execute (args , cwd = cwd )
230262
231263
232- # TODO: Properly integrate with a config dict (with callbacks)
233- _default_compiler = Compiler ()
234-
235-
236264class GNUCompiler (Compiler ):
265+ """
266+ GNU compiler configuration for gcc and gfortran
267+ """
237268
238269 CC = 'gcc'
239270 CFLAGS = ['-g' , '-fPIC' ]
240271 F90 = 'gfortran'
241272 F90FLAGS = ['-g' , '-fPIC' ]
273+ FC = 'gfortran'
274+ FCFLAGS = ['-g' , '-fPIC' ]
242275 LD = 'gfortran'
243- LDFLAGS = []
276+ LDFLAGS = ['-static' ]
277+ LD_STATIC = 'ar'
278+ LDFLAGS_STATIC = ['src' ]
279+ F2PY_FCOMPILER_TYPE = 'gnu95'
280+
281+ CC_PATTERN = re .compile (r'(^|/|\\)gcc\b' )
282+ FC_PATTERN = re .compile (r'(^|/|\\)gfortran\b' )
283+
284+
285+ class NvidiaCompiler (Compiler ):
286+ """
287+ NVHPC compiler configuration for nvc and nvfortran
288+ """
289+
290+ CC = 'nvc'
291+ CFLAGS = ['-g' , '-fPIC' ]
292+ F90 = 'nvfortran'
293+ F90FLAGS = ['-g' , '-fPIC' ]
294+ FC = 'nvfortran'
295+ FCFLAGS = ['-g' , '-fPIC' ]
296+ LD = 'nvfortran'
297+ LDFLAGS = ['-static' ]
298+ LD_STATIC = 'ar'
299+ LDFLAGS_STATIC = ['src' ]
300+ F2PY_FCOMPILER_TYPE = 'nv'
244301
302+ CC_PATTERN = re .compile (r'(^|/|\\)nvc\b' )
303+ FC_PATTERN = re .compile (r'(^|/|\\)(pgf9[05]|pgfortran|nvfortran)\b' )
304+
305+ def _mod_dir_args (self , mod_dir ):
306+ if mod_dir is None :
307+ return []
308+ return ['-module' , str (mod_dir )]
309+
310+
311+ def get_compiler_from_env (env = None ):
312+ """
313+ Utility function to determine what compiler to use
245314
246- class EscapeGNUCompiler (GNUCompiler ):
315+ This takes the following environment variables in the given order
316+ into account to determine the most likely compiler family:
317+ ``F90``, ``FC``, ``CC``.
247318
248- F90FLAGS = ['-O3' , '-g' , '-fPIC' ,
249- '-ffpe-trap=invalid,zero,overflow' , '-fstack-arrays' ,
250- '-fconvert=big-endian' ,
251- '-fbacktrace' ,
252- '-fno-second-underscore' ,
253- '-ffree-form' ,
254- '-ffast-math' ,
255- '-fno-unsafe-math-optimizations' ]
319+ Currently, :any:`GNUCompiler` and :any:`NvidiaCompiler` are available.
320+
321+ The compiler binary and flags can be further overwritten by setting
322+ the corresponding environment variables:
323+
324+ - ``CC``, ``FC``, ``F90``, ``LD`` for compiler/linker binary name or path
325+ - ``CFLAGS``, ``FCFLAGS``, ``LDFLAGS`` for compiler/linker flags to use
326+
327+ Parameters
328+ ----------
329+ env : dict, optional
330+ Use the specified environment (default: :any:`os.environ`)
331+
332+ Returns
333+ -------
334+ :any:`Compiler`
335+ A compiler object
336+ """
337+ if env is None :
338+ env = os .environ
339+
340+ candidates = (GNUCompiler , NvidiaCompiler )
341+ compiler = None
342+
343+ # "guess" the most likely compiler choice
344+ var_pattern_map = {
345+ 'F90' : 'FC_PATTERN' ,
346+ 'FC' : 'FC_PATTERN' ,
347+ 'CC' : 'CC_PATTERN'
348+ }
349+ for var , pattern in var_pattern_map .items ():
350+ if env .get (var ):
351+ for candidate in candidates :
352+ if getattr (candidate , pattern ).search (env [var ]):
353+ compiler = candidate ()
354+ debug (f'Environment variable { var } ={ env [var ]} set, using { candidate } ' )
355+ break
356+ else :
357+ continue
358+ break
359+
360+ if compiler is None :
361+ compiler = Compiler ()
362+
363+ # overwrite compiler executable and compiler flags with environment values
364+ var_compiler_map = {
365+ 'CC' : 'cc' ,
366+ 'FC' : 'fc' ,
367+ 'F90' : 'f90' ,
368+ 'LD' : 'ld' ,
369+ }
370+ for var , attr in var_compiler_map .items ():
371+ if var in env :
372+ setattr (compiler , attr , env [var ].strip ())
373+ debug (f'Environment variable { var } set, using custom compiler executable { env [var ]} ' )
374+
375+ var_flag_map = {
376+ 'CFLAGS' : 'cflags' ,
377+ 'FCFLAGS' : 'fcflags' ,
378+ 'LDFLAGS' : 'ldflags' ,
379+ }
380+ for var , attr in var_flag_map .items ():
381+ if var in env :
382+ setattr (compiler , attr , env [var ].strip ().split ())
383+ debug (f'Environment variable { var } set, overwriting compiler flags as { env [var ]} ' )
384+
385+ return compiler
386+
387+
388+ # TODO: Properly integrate with a config dict (with callbacks)
389+ _default_compiler = get_compiler_from_env ()
0 commit comments