From 5b799aabc2491c706916590d460ea303ea43d5fe Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Mon, 15 Sep 2025 23:17:21 +1000 Subject: [PATCH 1/4] #418 Added tool for pkg-config and nf-config. --- source/fab/tools/nf_config.py | 37 +++++++++++ source/fab/tools/pkg_config.py | 42 +++++++++++++ tests/unit_tests/tools/test_nf_config.py | 75 +++++++++++++++++++++++ tests/unit_tests/tools/test_pkg_config.py | 75 +++++++++++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 source/fab/tools/nf_config.py create mode 100644 source/fab/tools/pkg_config.py create mode 100644 tests/unit_tests/tools/test_nf_config.py create mode 100644 tests/unit_tests/tools/test_pkg_config.py diff --git a/source/fab/tools/nf_config.py b/source/fab/tools/nf_config.py new file mode 100644 index 00000000..4d09c30e --- /dev/null +++ b/source/fab/tools/nf_config.py @@ -0,0 +1,37 @@ +############################################################################## +# (c) Crown copyright Met Office. All rights reserved. +# For further details please refer to the file COPYRIGHT +# which you should have received as part of this distribution +############################################################################## + +"""This file contains the class to interface with NetCDF's nf-config script. +""" + +from typing import List + +from fab.tools.category import Category +from fab.tools.tool import Tool + + +class NfConfig(Tool): + '''This class interfaces with NetCDF's nf-config tool. It is not added + to the ToolRepository, it is intended for site-specific configurations + to make it easier to query for NetCDF settings. + ''' + + def __init__(self): + super().__init__("nf-config", "nf-config", Category.MISC) + + def get_compiler_flags(self) -> List[str]: + """ + :returns: the compilation flags to use for NetCDF. + """ + flags = self.run(additional_parameters=["--fflags"]) + return flags.split() + + def get_linker_flags(self) -> List[str]: + """ + :returns: the linker flags to use for NetCDF. + """ + flags = self.run(additional_parameters=["--flibs"]) + return flags.split() diff --git a/source/fab/tools/pkg_config.py b/source/fab/tools/pkg_config.py new file mode 100644 index 00000000..53ea462d --- /dev/null +++ b/source/fab/tools/pkg_config.py @@ -0,0 +1,42 @@ +############################################################################## +# (c) Crown copyright Met Office. All rights reserved. +# For further details please refer to the file COPYRIGHT +# which you should have received as part of this distribution +############################################################################## + +"""This file contains the class to interface with pkg-config. +""" + +from typing import List + +from fab.tools.category import Category +from fab.tools.tool import Tool + + +class PkgConfig(Tool): + '''This class implements a simple interface to `pkg-config`. PkgConfig is + not added to the ToolRepository, it is intended for site-specific + configurations to create an instance for each required package. + + :param name: the name of the package. It is the responsibility of the + user to ensure that package is really available. + + ''' + + def __init__(self, name: str): + super().__init__(f"pkg-config({name})", "pkg-config", Category.MISC) + self._package = name + + def get_compiler_flags(self) -> List[str]: + """ + :returns: the compilation flags to use for the specified package. + """ + flags = self.run(additional_parameters=[self._package, "--cflags"]) + return flags.split() + + def get_linker_flags(self) -> List[str]: + """ + :returns: the linker flags to use for the specified package. + """ + flags = self.run(additional_parameters=[self._package, "--libs"]) + return flags.split() diff --git a/tests/unit_tests/tools/test_nf_config.py b/tests/unit_tests/tools/test_nf_config.py new file mode 100644 index 00000000..c1d2ded1 --- /dev/null +++ b/tests/unit_tests/tools/test_nf_config.py @@ -0,0 +1,75 @@ +############################################################################## +# (c) Crown copyright Met Office. All rights reserved. +# For further details please refer to the file COPYRIGHT +# which you should have received as part of this distribution +############################################################################## +""" +Tests the nf-config wrapper. +""" + +from pytest_subprocess.fake_process import FakeProcess + +from tests.conftest import call_list + +from fab.tools.category import Category +from fab.tools.nf_config import NfConfig + + +def test_constructor() -> None: + """ + Tests default constructor. + """ + nfc = NfConfig() + assert nfc.category == Category.MISC + assert nfc.name == "nf-config" + assert nfc.exec_name == "nf-config" + assert nfc.get_flags() == [] + + +def test_nf_config_check_available(fake_process: FakeProcess) -> None: + """ + Tests availability functionality. + """ + fake_process.register(['nf-config', '--version'], + returncode=0, + stdout="netCDF-Fortran 4.6.1") + + nfc = NfConfig() + assert nfc.check_available() + assert call_list(fake_process) == [["nf-config", "--version"]] + + +def test_nf_config_check_unavailable(fake_process: FakeProcess) -> None: + """ + Tests availability failure. + """ + fake_process.register(['nf-config', '--version'], + returncode=127, + stderr="command 'nf-config' not found") + nfc = NfConfig() + assert not nfc.check_available() + assert call_list(fake_process) == [["nf-config", "--version"]] + + +def test_nf_config_compiler_flags(fake_process: FakeProcess) -> None: + """ + Tests getting the compiler flags. + """ + fake_process.register(['nf-config', '--fflags'], + returncode=0, + stdout="-I /somewhere") + nfc = NfConfig() + assert nfc.get_compiler_flags() == ["-I", "/somewhere"] + assert call_list(fake_process) == [["nf-config", "--fflags"]] + + +def test_nf_config_linker_flags(fake_process: FakeProcess) -> None: + """ + Tests availability failure. + """ + fake_process.register(['nf-config', '--flibs'], + returncode=0, + stdout="-L /somewhere -lsomewhat") + nfc = NfConfig() + assert nfc.get_linker_flags() == ["-L", "/somewhere", "-lsomewhat"] + assert call_list(fake_process) == [["nf-config", "--flibs"]] diff --git a/tests/unit_tests/tools/test_pkg_config.py b/tests/unit_tests/tools/test_pkg_config.py new file mode 100644 index 00000000..2dec4058 --- /dev/null +++ b/tests/unit_tests/tools/test_pkg_config.py @@ -0,0 +1,75 @@ +############################################################################## +# (c) Crown copyright Met Office. All rights reserved. +# For further details please refer to the file COPYRIGHT +# which you should have received as part of this distribution +############################################################################## +""" +Tests the pkg-config wrapper. +""" + +from pytest_subprocess.fake_process import FakeProcess + +from tests.conftest import call_list + +from fab.tools.category import Category +from fab.tools.pkg_config import PkgConfig + + +def test_constructor() -> None: + """ + Tests default constructor. + """ + pcf = PkgConfig('dummy') + assert pcf.category == Category.MISC + assert pcf.name == 'pkg-config(dummy)' + assert pcf.exec_name == 'pkg-config' + assert pcf.get_flags() == [] + + +def test_nf_config_check_available(fake_process: FakeProcess) -> None: + """ + Tests availability functionality. + """ + fake_process.register(['pkg-config', '--version'], + returncode=0, + stdout='netCDF-Fortran 4.6.1') + + pcf = PkgConfig('dummy') + assert pcf.check_available() + assert call_list(fake_process) == [['pkg-config', '--version']] + + +def test_nf_config_check_unavailable(fake_process: FakeProcess) -> None: + """ + Tests availability failure. + """ + fake_process.register(['pkg-config', '--version'], + returncode=127, + stderr="command 'pkg-config' not found") + pcf = PkgConfig("dummy") + assert not pcf.check_available() + assert call_list(fake_process) == [['pkg-config', '--version']] + + +def test_nf_config_compiler_flags(fake_process: FakeProcess) -> None: + """ + Tests getting the compiler flags. + """ + fake_process.register(['pkg-config', 'dummy', '--cflags'], + returncode=0, + stdout='-I /somewhere') + pcf = PkgConfig('dummy') + assert pcf.get_compiler_flags() == ['-I', '/somewhere'] + assert call_list(fake_process) == [['pkg-config', 'dummy', '--cflags']] + + +def test_nf_config_linker_flags(fake_process: FakeProcess) -> None: + """ + Tests availability failure. + """ + fake_process.register(['pkg-config', 'dummy', '--libs'], + returncode=0, + stdout='-L /somewhere -lsomewhat') + pcf = PkgConfig('dummy') + assert pcf.get_linker_flags() == ['-L', '/somewhere', '-lsomewhat'] + assert call_list(fake_process) == [['pkg-config', 'dummy', '--libs']] From 5ac1576ab4c10458d7620d661655ed03dd54237c Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Mon, 15 Sep 2025 23:50:58 +1000 Subject: [PATCH 2/4] #418 Added documentation. --- Documentation/source/fab_base/config.rst | 78 +++++++++++++++++++++++- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/Documentation/source/fab_base/config.rst b/Documentation/source/fab_base/config.rst index 056035d3..2afe43d8 100644 --- a/Documentation/source/fab_base/config.rst +++ b/Documentation/source/fab_base/config.rst @@ -74,7 +74,7 @@ general setting up the object, adding new tools to Fab's ``ToolRepository`` can be done here. ``get_valid_profiles`` ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ This method is called by ``FabBase`` when defining the command line options. It defines the list of valid compilation profile modes. This is used in setting up Python's ``ArgumentParser`` to only allow valid arguments. @@ -88,7 +88,7 @@ See :ref:`new_compilation_profiles` for an extended example. ``handle_command_line_options`` -------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This method is called immediately after calling the application-specific ``handle_command_line_options`` method. @@ -110,7 +110,7 @@ compiler: self._args = args ``update_toolbox`` ------------------- +~~~~~~~~~~~~~~~~~~ The ``update_toolbox`` method is called after the Fab ``ToolBox`` and ``BuildConfig`` objects have been created. All command line options have been parsed, and selected compilers have been added to @@ -179,3 +179,75 @@ from the user to setup flags for an Nvidia compiler: flags.extend(["-acc=cpu"]) ... nvfortran.add_flags(flags, "base") + + +Tools for site-specific configurations +-------------------------------------- +Fab provides some tool to simplify writing site-specific configuration +files: + +``NfConfig`` +~~~~~~~~~~~~ +The ``NfConfig`` class uses NetCDF's ``nf-config`` to query for compilation +and linking flags. Usage: + +.. code-block:: python + + from fab.tools.nf_config import NfConfig + + tr = ToolRepository() + linker = tr.get_tool(Category.LINKER, f"linker-{gfortran.name}") + linker = cast(Linker, linker) + + nf_config = NfConfig() + linker.add_lib_flags("netcdf", nf_config.get_linker_flags()) + + netcdf_compiler_flags = nf_config.get_compiler_flags() + +``PkgConfig`` +~~~~~~~~~~~~~ +This class provides a simple interface to ``pkg-config``. +Usage: + +.. code-block:: python + + from fab.tools.pkg_config import PkgConfig + + tr = ToolRepository() + linker = tr.get_tool(Category.LINKER, f"linker-{gfortran.name}") + linker = cast(Linker, linker) + + pkg_netcdf = PkgConfig("netcdf-fortran") + linker.add_lib_flags("netcdf", pkg_netcdf.get_linker_flags()) + + netcdf_compiler_flags = pkg_netcdf.get_compiler_flags() + +Note that at this stage no support for version numbers or version +checking has been added. + + +``Shell`` +~~~~~~~~~ +This class provides a simple interface to a shell, and it can be +used to easily start other scripts and use their output to set +flags. It takes the name of the shell as parameter. The `ToolRepository`` +contains a ready-to-go instance for ``sh``, but you can create an +instance that uses other shells. Usage: + +.. code-block:: python + + from fab.tools.shell import Shell + + # Get the pre-created `sh` shell: + shell = tr.get_default(Category.SHELL) + + bash = Shell("bash") + + try: + # We must remove the trailing new line, and create a list: + nc_flibs = shell.run(additional_parameters=["-c", "nf-config --flibs"], + capture_output=True).strip().split() + except RuntimeError: + nc_flibs = [] + + linker.add_lib_flags("netcdf", nc_flibs) From 12aa758a97adba47039e05d01200eb1f23081af9 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Tue, 16 Sep 2025 12:15:21 +1000 Subject: [PATCH 3/4] #418 Fix names in tests. --- tests/unit_tests/tools/test_pkg_config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/tools/test_pkg_config.py b/tests/unit_tests/tools/test_pkg_config.py index 2dec4058..b09856f0 100644 --- a/tests/unit_tests/tools/test_pkg_config.py +++ b/tests/unit_tests/tools/test_pkg_config.py @@ -26,7 +26,7 @@ def test_constructor() -> None: assert pcf.get_flags() == [] -def test_nf_config_check_available(fake_process: FakeProcess) -> None: +def test_pkg_config_check_available(fake_process: FakeProcess) -> None: """ Tests availability functionality. """ @@ -39,7 +39,7 @@ def test_nf_config_check_available(fake_process: FakeProcess) -> None: assert call_list(fake_process) == [['pkg-config', '--version']] -def test_nf_config_check_unavailable(fake_process: FakeProcess) -> None: +def test_pkg_config_check_unavailable(fake_process: FakeProcess) -> None: """ Tests availability failure. """ @@ -51,7 +51,7 @@ def test_nf_config_check_unavailable(fake_process: FakeProcess) -> None: assert call_list(fake_process) == [['pkg-config', '--version']] -def test_nf_config_compiler_flags(fake_process: FakeProcess) -> None: +def test_pkg_config_compiler_flags(fake_process: FakeProcess) -> None: """ Tests getting the compiler flags. """ @@ -63,7 +63,7 @@ def test_nf_config_compiler_flags(fake_process: FakeProcess) -> None: assert call_list(fake_process) == [['pkg-config', 'dummy', '--cflags']] -def test_nf_config_linker_flags(fake_process: FakeProcess) -> None: +def test_pkg_config_linker_flags(fake_process: FakeProcess) -> None: """ Tests availability failure. """ From bf71c2a34250967e26e2b8de6c4abff7e970f740 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Wed, 14 Jan 2026 22:43:31 +1100 Subject: [PATCH 4/4] #418 Added new tools to fab.api --- source/fab/api.py | 4 ++++ tests/unit_tests/test_api.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/source/fab/api.py b/source/fab/api.py index e447f0b2..3d118d06 100644 --- a/source/fab/api.py +++ b/source/fab/api.py @@ -31,6 +31,8 @@ from fab.tools.compiler import Compiler, Ifort from fab.tools.compiler_wrapper import CompilerWrapper from fab.tools.linker import Linker +from fab.tools.nf_config import NfConfig +from fab.tools.pkg_config import PkgConfig from fab.tools.tool import Tool from fab.tools.tool_box import ToolBox from fab.tools.tool_repository import ToolRepository @@ -69,6 +71,8 @@ "link_exe", "link_shared_object", "log_or_dot", + "NfConfig", + "PkgConfig", "preprocess_c", "preprocess_fortran", "preprocess_x90", diff --git a/tests/unit_tests/test_api.py b/tests/unit_tests/test_api.py index 31b7fe5f..5a5cb3cb 100644 --- a/tests/unit_tests/test_api.py +++ b/tests/unit_tests/test_api.py @@ -48,6 +48,8 @@ def test_import_from_api() -> None: "link_exe", "link_shared_object", "log_or_dot", + "NfConfig", + "PkgConfig", "preprocess_c", "preprocess_fortran", "preprocess_x90",