Skip to content

Commit

Permalink
Removed mixing, use a simple regex instead.
Browse files Browse the repository at this point in the history
  • Loading branch information
hiker committed Oct 17, 2024
1 parent 282f068 commit b9aabf8
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 115 deletions.
5 changes: 1 addition & 4 deletions source/fab/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
from fab.tools.ar import Ar
from fab.tools.category import Category
from fab.tools.compiler import (CCompiler, Compiler, FortranCompiler, Gcc,
Gfortran, GnuVersionHandling, Icc, Ifort,
IntelVersionHandling)
Gfortran, Icc, Ifort)
from fab.tools.compiler_wrapper import CompilerWrapper, Mpicc, Mpif90
from fab.tools.flags import Flags
from fab.tools.linker import Linker
Expand Down Expand Up @@ -39,10 +38,8 @@
"Gcc",
"Gfortran",
"Git",
"GnuVersionHandling",
"Icc",
"Ifort",
"IntelVersionHandling",
"Linker",
"Mpif90",
"Mpicc",
Expand Down
133 changes: 41 additions & 92 deletions source/fab/tools/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class Compiler(CompilerSuiteTool):
:param name: name of the compiler.
:param exec_name: name of the executable to start.
:param suite: name of the compiler suite this tool belongs to.
:param version_regex: A regular expression that allows extraction of
the version number from the version output of the compiler. The
version is taken from the first group of a match.
:param category: the Category (C_COMPILER or FORTRAN_COMPILER).
:param compile_flag: the compilation flag to use when only requesting
compilation (not linking).
Expand All @@ -46,6 +49,7 @@ class Compiler(CompilerSuiteTool):
def __init__(self, name: str,
exec_name: Union[str, Path],
suite: str,
version_regex: str,
category: Category,
mpi: bool = False,
compile_flag: Optional[str] = None,
Expand All @@ -60,6 +64,7 @@ def __init__(self, name: str,
self._output_flag = output_flag if output_flag else "-o"
self._openmp_flag = openmp_flag if openmp_flag else ""
self.flags.extend(os.getenv("FFLAGS", "").split())
self._version_regex = version_regex

@property
def mpi(self) -> bool:
Expand Down Expand Up @@ -149,7 +154,14 @@ def get_version(self) -> Tuple[int, ...]:
# Run the compiler to get the version and parse the output
# The implementations depend on vendor
output = self.run_version_command()
version_string = self.parse_version_output(self.category, output)

# Multiline is required in case that the version number is the end
# of the string, otherwise the $ would not match the end of line
matches = re.search(self._version_regex, output, re.MULTILINE)
if not matches:
raise RuntimeError(f"Unexpected version output format for "
f"compiler '{self.name}': {output}")
version_string = matches.groups()[0]

# Expect the version to be dot-separated integers.
try:
Expand Down Expand Up @@ -188,15 +200,6 @@ def run_version_command(
raise RuntimeError(f"Error asking for version of compiler "
f"'{self.name}'") from err

def parse_version_output(self, category: Category,
version_output: str) -> str:
'''
Extract the numerical part from the version output.
Implemented in specific compilers.
'''
raise NotImplementedError("The method `parse_version_output` must be "
"provided using a mixin.")

def get_version_string(self) -> str:
"""
Get a string representing the version of the given compiler.
Expand All @@ -219,6 +222,8 @@ class CCompiler(Compiler):
:param name: name of the compiler.
:param exec_name: name of the executable to start.
:param suite: name of the compiler suite.
:param version_regex: A regular expression that allows extraction of
the version number from the version output of the compiler.
:param mpi: whether the compiler or linker support MPI.
:param compile_flag: the compilation flag to use when only requesting
compilation (not linking).
Expand All @@ -229,14 +234,16 @@ class CCompiler(Compiler):

# pylint: disable=too-many-arguments
def __init__(self, name: str, exec_name: str, suite: str,
version_regex: str,
mpi: bool = False,
compile_flag: Optional[str] = None,
output_flag: Optional[str] = None,
openmp_flag: Optional[str] = None):
super().__init__(name, exec_name, suite,
category=Category.C_COMPILER, mpi=mpi,
compile_flag=compile_flag, output_flag=output_flag,
openmp_flag=openmp_flag)
openmp_flag=openmp_flag,
version_regex=version_regex)


# ============================================================================
Expand All @@ -248,6 +255,8 @@ class FortranCompiler(Compiler):
:param name: name of the compiler.
:param exec_name: name of the executable to start.
:param suite: name of the compiler suite.
:param version_regex: A regular expression that allows extraction of
the version number from the version output of the compiler.
:param mpi: whether MPI is supported by this compiler or not.
:param compile_flag: the compilation flag to use when only requesting
compilation (not linking).
Expand All @@ -262,6 +271,7 @@ class FortranCompiler(Compiler):

# pylint: disable=too-many-arguments
def __init__(self, name: str, exec_name: str, suite: str,
version_regex: str,
mpi: bool = False,
compile_flag: Optional[str] = None,
output_flag: Optional[str] = None,
Expand All @@ -273,7 +283,8 @@ def __init__(self, name: str, exec_name: str, suite: str,
super().__init__(name=name, exec_name=exec_name, suite=suite,
category=Category.FORTRAN_COMPILER,
mpi=mpi, compile_flag=compile_flag,
output_flag=output_flag, openmp_flag=openmp_flag)
output_flag=output_flag, openmp_flag=openmp_flag,
version_regex=version_regex)
self._module_folder_flag = (module_folder_flag if module_folder_flag
else "")
self._syntax_only_flag = syntax_only_flag
Expand Down Expand Up @@ -327,45 +338,7 @@ def compile_file(self, input_file: Path,


# ============================================================================
class GnuVersionHandling():
'''Mixin to handle version information from GNU compilers'''

def parse_version_output(self, category: Category,
version_output: str) -> str:
'''
Extract the numerical part from a GNU compiler's version output
:param name: the compiler's name
:param category: the compiler's Category
:param version_output: the full version output from the compiler
:returns: the actual version as a string
:raises RuntimeError: if the output is not in an expected format.
'''

# Expect the version to appear after some in parentheses, e.g.
# "GNU Fortran (...) n.n[.n, ...]" or # "gcc (...) n.n[.n, ...]"
if category is Category.FORTRAN_COMPILER:
name = "GNU Fortran"
else:
name = "gcc"
# A version number is a digit, followed by a sequence of digits and
# '.'', ending with a digit. It must then be followed by either the
# end of the string, or a space (e.g. "... 5.6 123456"). We can't use
# \b to determine the end, since then "1.2." would be matched
# excluding the dot (so it would become a valid 1.2)
exp = name + r" \(.*?\) (\d[\d\.]+\d)(?:$| )"
# Multiline is required in case that the version number is the
# end of the string, otherwise the $ would not match the end of line
matches = re.search(exp, version_output, re.MULTILINE)
if not matches:
raise RuntimeError(f"Unexpected version output format for "
f"compiler '{name}': {version_output}")
return matches.groups()[0]


# ============================================================================
class Gcc(GnuVersionHandling, CCompiler):
class Gcc(CCompiler):
'''Class for GNU's gcc compiler.
:param name: name of this compiler.
Expand All @@ -375,12 +348,18 @@ def __init__(self,
name: str = "gcc",
exec_name: str = "gcc",
mpi: bool = False):
# A version number is a digit, followed by a sequence of digits and
# '.'', ending with a digit. It must then be followed by either the
# end of the string, or a space (e.g. "... 5.6 123456"). We can't use
# \b to determine the end, since then "1.2." would be matched
# excluding the dot (so it would become a valid 1.2)
super().__init__(name, exec_name, suite="gnu", mpi=mpi,
openmp_flag="-fopenmp")
openmp_flag="-fopenmp",
version_regex=r"gcc \(.*?\) (\d[\d\.]+\d)(?:$| )")


# ============================================================================
class Gfortran(GnuVersionHandling, FortranCompiler):
class Gfortran(FortranCompiler):
'''Class for GNU's gfortran compiler.
:param name: name of this compiler.
Expand All @@ -392,45 +371,13 @@ def __init__(self, name: str = "gfortran",
super().__init__(name, exec_name, suite="gnu",
openmp_flag="-fopenmp",
module_folder_flag="-J",
syntax_only_flag="-fsyntax-only")
syntax_only_flag="-fsyntax-only",
version_regex=(r"GNU Fortran \(.*?\) "
r"(\d[\d\.]+\d)(?:$| )"))


# ============================================================================
class IntelVersionHandling():
'''Mixin to handle version information from Intel compilers'''

def parse_version_output(self, category: Category,
version_output: str) -> str:
'''
Extract the numerical part from an Intel compiler's version output
:param name: the compiler's name
:param version_output: the full version output from the compiler
:returns: the actual version as a string
:raises RuntimeError: if the output is not in an expected format.
'''

# Expect the version to appear after some in parentheses, e.g.
# "icc (...) n.n[.n, ...]" or "ifort (...) n.n[.n, ...]"
if category == Category.C_COMPILER:
name = "icc"
else:
name = "ifort"

# A version number is a digit, followed by a sequence of digits and
# '.'', ending with a digit. It must then be followed by a space.
exp = name + r" \(.*?\) (\d[\d\.]+\d) "
matches = re.search(exp, version_output)

if not matches:
raise RuntimeError(f"Unexpected version output format for "
f"compiler '{name}': {version_output}")
return matches.groups()[0]


# ============================================================================
class Icc(IntelVersionHandling, CCompiler):
class Icc(CCompiler):
'''Class for the Intel's icc compiler.
:param name: name of this compiler.
Expand All @@ -439,11 +386,12 @@ class Icc(IntelVersionHandling, CCompiler):

def __init__(self, name: str = "icc", exec_name: str = "icc"):
super().__init__(name, exec_name, suite="intel-classic",
openmp_flag="-qopenmp")
openmp_flag="-qopenmp",
version_regex=r"icc \(ICC\) (\d[\d\.]+\d) ")


# ============================================================================
class Ifort(IntelVersionHandling, FortranCompiler):
class Ifort(FortranCompiler):
'''Class for Intel's ifort compiler.
:param name: name of this compiler.
Expand All @@ -454,4 +402,5 @@ def __init__(self, name: str = "ifort", exec_name: str = "ifort"):
super().__init__(name, exec_name, suite="intel-classic",
module_folder_flag="-module",
openmp_flag="-qopenmp",
syntax_only_flag="-syntax-only")
syntax_only_flag="-syntax-only",
version_regex=r"ifort \(IFORT\) (\d[\d\.]+\d) ")
5 changes: 1 addition & 4 deletions source/fab/tools/compiler_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,9 @@ def __init__(self, name: str, exec_name: str,
name=name, exec_name=exec_name,
category=self._compiler.category,
suite=self._compiler.suite,
version_regex=self._compiler._version_regex,
mpi=mpi,
availability_option=self._compiler.availability_option)
# We need to have the right version to parse the version output
# So we set this function based on the function that the
# wrapped compiler uses:
setattr(self, "parse_version_output", compiler.parse_version_output)

def __str__(self):
return f"{type(self).__name__}({self._compiler.name})"
Expand Down
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
@pytest.fixture(name="mock_c_compiler")
def fixture_mock_c_compiler():
'''Provides a mock C-compiler.'''
mock_compiler = CCompiler("mock_c_compiler", "mock_exec", "suite")
mock_compiler = CCompiler("mock_c_compiler", "mock_exec", "suite",
version_regex="something")
mock_compiler.run = mock.Mock()
mock_compiler._version = (1, 2, 3)
mock_compiler._name = "mock_c_compiler"
Expand All @@ -32,6 +33,7 @@ def fixture_mock_fortran_compiler():
'''Provides a mock Fortran-compiler.'''
mock_compiler = FortranCompiler("mock_fortran_compiler", "mock_exec",
"suite", module_folder_flag="",
version_regex="something",
syntax_only_flag=None, compile_flag=None,
output_flag=None, openmp_flag=None)
mock_compiler.run = mock.Mock()
Expand Down
23 changes: 11 additions & 12 deletions tests/unit_tests/tools/test_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@

def test_compiler():
'''Test the compiler constructor.'''
cc = Compiler("gcc", "gcc", "gnu", category=Category.C_COMPILER, openmp_flag="-fopenmp")
cc = Compiler("gcc", "gcc", "gnu", version_regex="some_regex",
category=Category.C_COMPILER, openmp_flag="-fopenmp")
assert cc.category == Category.C_COMPILER
assert cc._compile_flag == "-c"
assert cc._output_flag == "-o"
Expand All @@ -29,13 +30,9 @@ def test_compiler():
assert cc.suite == "gnu"
assert not cc.mpi
assert cc.openmp_flag == "-fopenmp"
with pytest.raises(NotImplementedError) as err:
cc.parse_version_output(Category.FORTRAN_COMPILER, "NOT NEEDED")
assert ("The method `parse_version_output` must be provided using a mixin."
in str(err.value))

fc = FortranCompiler("gfortran", "gfortran", "gnu", openmp_flag="-fopenmp",
module_folder_flag="-J")
version_regex="something", module_folder_flag="-J")
assert fc._compile_flag == "-c"
assert fc._output_flag == "-o"
assert fc.category == Category.FORTRAN_COMPILER
Expand All @@ -44,10 +41,6 @@ def test_compiler():
assert fc.flags == []
assert not fc.mpi
assert fc.openmp_flag == "-fopenmp"
with pytest.raises(NotImplementedError) as err:
fc.parse_version_output(Category.FORTRAN_COMPILER, "NOT NEEDED")
assert ("The method `parse_version_output` must be provided using a mixin."
in str(err.value))


def test_compiler_check_available():
Expand Down Expand Up @@ -121,16 +114,19 @@ def test_compiler_with_env_fflags():
def test_compiler_syntax_only():
'''Tests handling of syntax only flags.'''
fc = FortranCompiler("gfortran", "gfortran", "gnu",
version_regex="something",
openmp_flag="-fopenmp", module_folder_flag="-J")
# Empty since no flag is defined
assert not fc.has_syntax_only

fc = FortranCompiler("gfortran", "gfortran", "gnu", openmp_flag="-fopenmp",
module_folder_flag="-J", syntax_only_flag=None)
version_regex="something", module_folder_flag="-J",
syntax_only_flag=None)
# Empty since no flag is defined
assert not fc.has_syntax_only

fc = FortranCompiler("gfortran", "gfortran", "gnu",
version_regex="something",
openmp_flag="-fopenmp",
module_folder_flag="-J",
syntax_only_flag="-fsyntax-only")
Expand All @@ -141,6 +137,7 @@ def test_compiler_syntax_only():
def test_compiler_without_openmp():
'''Tests that the openmp flag is not used when openmp is not enabled. '''
fc = FortranCompiler("gfortran", "gfortran", "gnu",
version_regex="something",
openmp_flag="-fopenmp",
module_folder_flag="-J",
syntax_only_flag="-fsyntax-only")
Expand All @@ -157,6 +154,7 @@ def test_compiler_with_openmp():
'''Tests that the openmp flag is used as expected if openmp is enabled.
'''
fc = FortranCompiler("gfortran", "gfortran", "gnu",
version_regex="something",
openmp_flag="-fopenmp",
module_folder_flag="-J",
syntax_only_flag="-fsyntax-only")
Expand All @@ -172,7 +170,7 @@ def test_compiler_with_openmp():
def test_compiler_module_output():
'''Tests handling of module output_flags.'''
fc = FortranCompiler("gfortran", "gfortran", suite="gnu",
module_folder_flag="-J")
version_regex="something", module_folder_flag="-J")
fc.set_module_output_path("/module_out")
assert fc._module_output_path == "/module_out"
fc.run = mock.MagicMock()
Expand All @@ -185,6 +183,7 @@ def test_compiler_module_output():
def test_compiler_with_add_args():
'''Tests that additional arguments are handled as expected.'''
fc = FortranCompiler("gfortran", "gfortran", suite="gnu",
version_regex="something",
openmp_flag="-fopenmp",
module_folder_flag="-J")
fc.set_module_output_path("/module_out")
Expand Down
Loading

0 comments on commit b9aabf8

Please sign in to comment.