Scientific Computing

CMake ExternalProject pass lists

CMake ExternalProject builds arbitrary projects without interacting with the top level CMake project. Via ExternalProject, CMake can build child projects for virtually any build system including Make, Autotools, Meson, CMake, etc. A distinct technique is needed to pass a CMake list to ExternalProject. As a reminder, CMake lists are defined as semicolon-separated strings.

ExternalProject example of passing lists:

include(ExternalProject)

set(my_list "apple;cat")

ExternalProject_Add(child
...
CMAKE_ARGS --install-prefix=${CMAKE_INSTALL_PREFIX}
CMAKE_CACHE_ARGS -Dmyvar:STRING=${my_list}
)

In this example the CMAKE_ARGS are arbitrary, we just put some typical arguments. The key syntax to observe are that CMAKE_CACHE_ARGS is used to pass any lists. A reason this is necessary is that CMAKE_ARGS is passed as a command line. CMAKE_ARGS can also have problems if many long arguments are used, particularly on Windows where the maximum command line length could be exceeded. We don’t use CMAKE_CACHE_ARGS carte blanche because some arguments may not be intended to be cache variables.

NOTE: the variable type must be passed to CMAKE_CACHE_ARGS, as when setting any cache variable.

SSH-VNC client on Windows

Securely connect to a VNC server tunneled over SSH from Windows with this procedure. We use SSH tunneling for the insecure VNC protocol as for other insecure protocols like RDP. This procedure works from a local Windows PC to a remote VNC server on Linux, macOS, etc. We assume the remote server computer has an SSH server and VNC server already setup.

On the local client computer, add to the ~/.ssh/config for this Host a line like:

  LocalForward 5990 localhost:5900

where 5900 is the VNC port on the remote PC, and 5990 is an arbitrary distinct port on the local PC to avoid clashes on 5900. Note: the VNC server might use port other than 5900. On Linux a common default VNC port is 5901.

To connect, ssh to the server as usual, then use VNC Viewer to connect to localhost::5990. When the SSH connection (tunnel) is closed, so is the VNC connection.

Reference: macOS network ports

Build CMake from dev sources

While CMake binaries can be downloaded for most platforms, there are certain cases where one wishes to build CMake from source. For example, when preparing a merge request to fix or enhance CMake. Usually the computer will have at least an older version of CMake that can be used. If so, we recommend using CMake to build the newer CMake, from the CMake source directory. We recommend using Ninja in general for faster build and rebuild for any CMake project.

cmake -B build -DBUILD_TESTING:BOOL=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_USE_OPENSSL:BOOL=ON --install-prefix=$HOME/cmake-dev

cmake --build build

cmake --install build

It’s easier to simply use the scripts/build_cmake.cmake.


If an old CMake isn’t available on the computer, then use CMake bootstrap:

./bootstrap --prefix=$HOME/local/cmake-dev --parallel=4 -- -DBUILD_TESTING:BOOL=OFF -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_USE_OPENSSL:BOOL=ON

make -j4

make install

The OpenSSL flag is particularly important for CMake use connecting to the internet.

This puts the compiled CMake under ~/local/cmake-dev, without disturbing the primary CMake install. Upon making any CMake code changes, simply recompile the minimum needed bits by:

make install

Pytest stdout/stderr capture

Pytest captures stdout/stderr and sets stdin to os.devnull. When a test involves the use of Python subprocess, the “capfd” Pytest fixture is required to capture the subprocess output. “capsys” fixture only captures native Python prints, without capturing subprocess output. This is also true when testing a Python script intended to be used from the command line.

import subprocess
import sys

def test_script(capfd):
    """test that a particular message is printed to terminal"""
    ret = subprocess.run([sys.executable, "-m", "mymod"], text=True)
    assert ret.returncode == 0

    cap = capfd.readouterr()
    assert cap.out.strip() == "success"
    assert cap.err.strip() == ""

Mac Mini vs. Macbook value

The Mac Mini can be considerably cheaper than MacBooks, making the Mac Mini a good value for developers who work at a desk.

Running benchmarks that take about ten minutes, the Mac Mini fan did not come on and the case was still cool to the touch. If building large projects, consider the amount of RAM to avoid needing to limit build parallelism to avoid running out of RAM on build.

The Mac Mini has a modestly adequate internal speaker suitable for quiet offices. With macOS, the volume was not controllable in macOS itself with HDMI audio on the same monitor that volume control worked with Raspberry Pi–to be clear, this is an OS setting, not adjusting the monitor itself. It’s also disappointing that a microphone isn’t built in–this would have been useful for Siri at least. Perhaps Apple may have felt people would try to use an internal Mac Mini microphone for conferencing, and then get disappointed if they weren’t close enough to it for good sound.

As compared to a cloud physical Mac service, the Mac Mini pays for itself in a year, while adding value as a media center and convenience of having a local physical Mac.

CMake generate pkg-config .pc

CMake can generate pkg-config .pc files for packages. The .pc file can be used by many build systems. While we normally use the .cmake files for packages, we also include the .pc files for non-CMake users. Meson has a built-in function for generating pkg-config files, but CMake does not yet.

A good basic reference for pkg-config .pc syntax is helpful. We use a my_package.pc.in template with contents generated by CMake configure_file() and associated variables.

CMake NO_DEFAULT_PATH also disables hints and path suffixes

CMake “find_{file,library,package,path,program}” have a NO_DEFAULT_PATH parameter that disables ALL search paths, even the <NAME>_ROOT priority. The need for this and the workaround is best shown by example with a non-system compiler such as Intel oneAPI. The reason that we use NO_DEFAULT_PATH for non-system compilers is because CMake will still try to use the system libraries that may not be ABI compatible with the other compiler. NO_DEFAULT_PATH disables the CMake default PATH_SUFFIXES, so those need to be specified as well.

To make the command line and environment variable hints work again, do like:

find_library(ZLIB_LIBRARY
  NAMES z zlib
  NO_DEFAULT_PATH
  HINTS ${ZLIB_ROOT} ENV ZLIB_ROOT
  PATH_SUFFIXES lib)

find_path(ZLIB_INCLUDE_DIR
  NAMES zlib.h
  NO_DEFAULT_PATH
  HINTS ${ZLIB_ROOT} ENV ZLIB_ROOT
  PATH_SUFFIXES include)

Fortran generic interface procedure with optional arguments

Fortran 2018 added syntax useful for polymorphism including select rank, which allows rank polymorphism and select type, which allows class polymorphism. For reference, GCC ≥ 7 has select type and GCC ≥ 10 has select rank. Intel oneAPI supports both.

To be compatible with older compilers as well as handle cases where it’s simply more concise to use Fortran 2003 generic procedure interfaces, each procedure must be unambiguously distinguishable. Several criteria make a procedure distinguishable. Here we focus on having at least one non-optional argument that is TKR-distinct (Type, Kind, Rank).

Notice that this example has the first variable non-optional to make the procedures TKR-distinct.

module mod1

implicit none (type, external)

interface manyranks
  procedure m0,m1
end interface manyranks

private
public :: manyranks

contains

subroutine m0(val1, val2)
real, intent(in) :: val1
real, intent(in), optional :: val2
!! omitted code
end subroutine m0

subroutine m1(val1, val2)
real, intent(in) :: val1(:)
real, intent(in), optional :: val2(:)
!! omitted code
end subroutine m1

end module mod1

The code will compile as above. If you add optional to both “val1”, the compilation will fail like:

  • GCC: Ambiguous interfaces in generic interface 'manyranks' for 'm0' at (1) and 'm1' at (2)
  • Intel: #5286: Ambiguous generic interface MANYRANKS: previously declared specific procedure M0 is not distinguishable from this declaration. [M1]

Although this example used rank polymorphism, the same issue arises when using any of type, kind or rank (TKR) generic procedures–there must be an unambiguous resolution with at least one non-optional argument. This polymorphism is implemented at runtime, and so there is no guarantee of non-ambiguity when all arguments are optional.

Get list of CMake test names

CMake add_test() can be dynamically set by arbitrarily complex foreach(), if(), etc. logic. A subset of tests can be configured dynamically after the tests are declared. A list of tests enabled is retrieved by the TESTS directory property. The variable “test_names” contains all the test names previously added in the CMakeLists.txt via “add_test()” in a list in the DIRECTORY scope.

get_property(test_names DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY TESTS)

set_property(TEST ${test_names} PROPERTY TIMEOUT 60)

get_property(test_names GLOBAL PROPERTY TESTS) will return an empty list–DIRECTORY scope must be used.