Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 75 additions & 3 deletions Documentation/source/fab_base/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -87,7 +87,7 @@ profiles into account and set them up automatically.
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.

Expand All @@ -109,7 +109,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
Expand Down Expand Up @@ -178,3 +178,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)
4 changes: 4 additions & 0 deletions source/fab/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -69,6 +71,8 @@
"link_exe",
"link_shared_object",
"log_or_dot",
"NfConfig",
"PkgConfig",
"preprocess_c",
"preprocess_fortran",
"preprocess_x90",
Expand Down
37 changes: 37 additions & 0 deletions source/fab/tools/nf_config.py
Original file line number Diff line number Diff line change
@@ -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()
42 changes: 42 additions & 0 deletions source/fab/tools/pkg_config.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 2 additions & 0 deletions tests/unit_tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
75 changes: 75 additions & 0 deletions tests/unit_tests/tools/test_nf_config.py
Original file line number Diff line number Diff line change
@@ -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"]]
75 changes: 75 additions & 0 deletions tests/unit_tests/tools/test_pkg_config.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test is needed that pkg_config is honouring the PKG_CONFIG_LIBDIR and PKG_CONFIG_PATH environment variables. Particularly that last one as it is how new package collections are added to the system.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should assume that pkg-config is installed on the system (while we control gitlab ci, and many other systems, so we can ensure it is available, in general I don't think we shouldThat's also the reason why I tried to clean up tests that relied on gcc or so being available).

Then, how should I test that the environment variable are passed on? Creating a shell script that echo's the environment? Seems a bit odd - if this is to be tested, it should be done with the base class to verify it does not add/remove anything from the environment instead? Should I do that?

Original file line number Diff line number Diff line change
@@ -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_pkg_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_pkg_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_pkg_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_pkg_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']]