diff --git a/source/fab/tools/__init__.py b/source/fab/tools/__init__.py index 45eb666f..baa06c01 100644 --- a/source/fab/tools/__init__.py +++ b/source/fab/tools/__init__.py @@ -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 @@ -39,10 +38,8 @@ "Gcc", "Gfortran", "Git", - "GnuVersionHandling", "Icc", "Ifort", - "IntelVersionHandling", "Linker", "Mpif90", "Mpicc", diff --git a/source/fab/tools/compiler.py b/source/fab/tools/compiler.py index 6566a292..6935a3ff 100644 --- a/source/fab/tools/compiler.py +++ b/source/fab/tools/compiler.py @@ -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). @@ -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, @@ -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: @@ -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: @@ -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. @@ -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). @@ -229,6 +234,7 @@ 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, @@ -236,7 +242,8 @@ def __init__(self, name: str, exec_name: str, suite: str, 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) # ============================================================================ @@ -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). @@ -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, @@ -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 @@ -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. @@ -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. @@ -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. @@ -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. @@ -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) ") diff --git a/source/fab/tools/compiler_wrapper.py b/source/fab/tools/compiler_wrapper.py index e54f98ea..4dc24199 100644 --- a/source/fab/tools/compiler_wrapper.py +++ b/source/fab/tools/compiler_wrapper.py @@ -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})" diff --git a/tests/conftest.py b/tests/conftest.py index 559d4f3b..86de6476 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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" @@ -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() diff --git a/tests/unit_tests/tools/test_compiler.py b/tests/unit_tests/tools/test_compiler.py index ac948246..ff4ec01b 100644 --- a/tests/unit_tests/tools/test_compiler.py +++ b/tests/unit_tests/tools/test_compiler.py @@ -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" @@ -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 @@ -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(): @@ -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") @@ -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") @@ -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") @@ -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() @@ -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") diff --git a/tests/unit_tests/tools/test_tool_box.py b/tests/unit_tests/tools/test_tool_box.py index b8e2e903..965fe4f0 100644 --- a/tests/unit_tests/tools/test_tool_box.py +++ b/tests/unit_tests/tools/test_tool_box.py @@ -43,9 +43,11 @@ def test_tool_box_add_tool_replacement(): warning can be disabled.''' tb = ToolBox() - mock_compiler1 = CCompiler("mock_c_compiler1", "mock_exec1", "suite") + mock_compiler1 = CCompiler("mock_c_compiler1", "mock_exec1", "suite", + version_regex="something") mock_compiler1._is_available = True - mock_compiler2 = CCompiler("mock_c_compiler2", "mock_exec2", "suite") + mock_compiler2 = CCompiler("mock_c_compiler2", "mock_exec2", "suite", + version_regex="something") mock_compiler2._is_available = True tb.add_tool(mock_compiler1)