Skip to content

Commit 9ba0b96

Browse files
authored
Merge pull request #294 from ecmwf-ifs/nabr-macos-compatibility
Consistent, environment-configurable use of Compiler class in JIT compilation
2 parents c212156 + d3aca00 commit 9ba0b96

File tree

3 files changed

+228
-45
lines changed

3 files changed

+228
-45
lines changed

loki/build/compiler.py

Lines changed: 177 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,20 @@
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
98
from importlib import import_module, reload
9+
import os
10+
import re
1011
import 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

2024
def 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-
236264
class 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()

loki/build/jit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def jit_compile(source, filepath=None, objname=None):
5454
filepath = Path(filepath)
5555
Sourcefile(filepath).write(source=source)
5656

57-
pymod = compile_and_load(filepath, cwd=str(filepath.parent), use_f90wrap=True, f90wrap_kind_map=_f90wrap_kind_map)
57+
pymod = compile_and_load(filepath, cwd=str(filepath.parent), f90wrap_kind_map=_f90wrap_kind_map)
5858

5959
if objname:
6060
return getattr(pymod, objname)

loki/build/tests/test_build.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
from pathlib import Path
99
import pytest
1010

11-
from loki.build import Obj, Lib, Builder
11+
from loki.build import (
12+
Obj, Lib, Builder,
13+
Compiler, GNUCompiler, NvidiaCompiler, get_compiler_from_env, _default_compiler
14+
)
1215

1316

1417
@pytest.fixture(scope='module', name='path')
@@ -112,3 +115,49 @@ def test_build_binary(builder):
112115
Test basic binary compilation from objects and libs.
113116
"""
114117
assert builder
118+
119+
120+
@pytest.mark.parametrize('env,cls,attrs', [
121+
# Overwrite completely custom
122+
(
123+
{'CC': 'my-weird-compiler', 'FC': 'my-other-weird-compiler', 'F90': 'weird-fortran', 'FCFLAGS': '-my-flag '},
124+
Compiler,
125+
{'CC': 'my-weird-compiler', 'FC': 'my-other-weird-compiler', 'F90': 'weird-fortran', 'FCFLAGS': ['-my-flag']},
126+
),
127+
# GNUCompiler
128+
({'CC': 'gcc'}, GNUCompiler, {'CC': 'gcc', 'FC': 'gfortran', 'F90': 'gfortran'}),
129+
({'CC': 'gcc-13'}, GNUCompiler, None),
130+
({'CC': '/path/to/my/gcc'}, GNUCompiler, None),
131+
({'CC': '../../relative/path/to/my/gcc-11'}, GNUCompiler, None),
132+
({'CC': 'C:\\windows\\path\\to\\gcc'}, GNUCompiler, None),
133+
({'FC': 'gfortran'}, GNUCompiler, None),
134+
({'FC': 'gfortran-13', 'FCFLAGS': '-O3 -g'}, GNUCompiler, {'FC': 'gfortran-13', 'FCFLAGS': ['-O3', '-g']}),
135+
({'FC': '/path/to/my/gfortran'}, GNUCompiler, None),
136+
({'FC': '../../relative/path/to/my/gfortran'}, GNUCompiler, None),
137+
({'FC': 'C:\\windows\\path\\to\\gfortran'}, GNUCompiler, None),
138+
# NvidiaCompiler
139+
({'FC': 'nvfortran'}, NvidiaCompiler, {'CC': 'nvc', 'FC': 'nvfortran', 'F90': 'nvfortran'}),
140+
({'CC': 'nvc'}, NvidiaCompiler, None),
141+
({'CC': '/path/to/my/nvc'}, NvidiaCompiler, None),
142+
({'CC': '../../relative/path/to/my/nvc'}, NvidiaCompiler, None),
143+
({'CC': 'C:\\windows\\path\\to\\nvc'}, NvidiaCompiler, None),
144+
({'FC': 'pgf90'}, NvidiaCompiler, None),
145+
({'FC': 'pgf95'}, NvidiaCompiler, None),
146+
({'FC': 'pgfortran'}, NvidiaCompiler, None),
147+
({'FC': '/path/to/my/nvfortran'}, NvidiaCompiler, None),
148+
({'FC': '../../relative/path/to/my/pgfortran'}, NvidiaCompiler, None),
149+
({'FC': 'C:\\windows\\path\\to\\nvfortran'}, NvidiaCompiler, None),
150+
])
151+
def test_get_compiler_from_env(env, cls, attrs):
152+
compiler = get_compiler_from_env(env)
153+
assert type(compiler) == cls # pylint: disable=unidiomatic-typecheck
154+
for attr, expected_value in (attrs or env).items():
155+
# NB: We are comparing the lower-case attribute
156+
# because that contains the runtime value
157+
assert getattr(compiler, attr.lower()) == expected_value
158+
159+
160+
def test_default_compiler():
161+
# Check that _default_compiler corresponds to a call with None
162+
compiler = get_compiler_from_env()
163+
assert type(compiler) == type(_default_compiler) # pylint: disable=unidiomatic-typecheck

0 commit comments

Comments
 (0)