Scientific Computing

CMake dependency graph

CMake --graphviz and graphviz configure preset can generate GraphViz dependency graphs for CMake-supported project code languages including C, C++, and Fortran. Fortran executables and modules are shown in the directed dependency graph. Fortran submodule are not shown in the graph.

The “dot” GraphViz program converts the .dot files to PNG, SVG, etc. dot program is available by installing the “graphviz” program via the package manager.

Generating the dependency graph requires CMake configure and generate. Thus, the compiler and generator needed by the CMake project must be working. The project does not need to be compiled before generating the dependency graph. However, the user should select the same CMake configure options as they would for compiling the project.

Example: h5fortran HDF5 object-oriented Fortran dependency graph is below. SVG vector graphics can be zoomed arbitrarily large in a web browser. The “gfx/” directory is to avoid making files in the source directory.

cmake -B build --graphviz=gfx/block.dot

cd gfx

dot -Tpng -o block.png block.dot

dot -Tsvg -o block.svg block.dot

h5fortran dependency graph

The “dependers” files show only the nodes depending on a node.

Scripts and Viewing output

CMakeUtils graph.py converts a directory of CMake dot diagrams to SVG or PNG and collects them in an HTML document for easy viewing:

python cmakeutils/graph.py ~/myprog/gfx

To open a web browser from the Terminal:

python -m webbrowser -t file:///$HOME/myprog/gfx/index.html

Note that “file:///” has three slashes and the file path must be absolute.


Related: Dependency graphs are also easily created in Python and Matlab.

Find executable path in Python

The full path to executables on the system Path (and cwd on Windows) are discovered by Python shutil.which. On Windows, environment variable PATHEXT is used to search filename suffixes if not specified at the input to shutil.which().

Shell aliases are not found by shutil.which() since the shell is not invoked. Instead append the directory of the desired executable to environment variable PATH, or specify it in shutil.which(..., path="/path/to/exe").

import shutil

# None if executable not found
exe = shutil.which('ls')

Since shutil.which() returns None for non-found executable it is convenient for pytest.mark.skipif

For programs not on PATH where the executable path is known:

shutil.which('myexe', path="/path/to/myexe")

CMake find_program script

CMake find_program does not generally consider NAMES parameter to have file suffixes unless manually specified. For Windows, .com and .exe file suffixes are considered, with search order:

  1. .com
  2. .exe
  3. no suffix

If on Windows and an executable “hello.exe” or “hello.com” exists, then CMake will find it. CMake would NOT find “hello.exe” on non-Windows platforms, where no file suffix is expected.

The full path to executables on the system Path (and cwd on Windows) are found by find_program(). Shell aliases are not found since the shell is not invoked. Instead specify find_program(... HINTS /path/to/exe).

ℹ️ Note

CMAKE_EXECUTABLE_SUFFIX ONLY affects find_program() in CMake role PROJECT

find_program(v NAMES hello)

Shell scripts of any file suffix on any operating system are found iff:

  1. (non-Windows) script file executable flag is set (find_program general requirement)
  2. script file suffix is specified as part of find_program(… NAMES) parameter

A complete standalone example:

Detect CMake generator from CMakeCache.txt

The CMAKE_GENERATOR cache variable records the CMake generator used to configure the CMake project in the CMakeCache.txt CMake cache file. Surprisingly, the cmake -LA option does not emit the CMAKE_GENERATOR value.

Thus, parsing CMakeCache.txt will give the previously used CMake generator. This is relevant in automated processes such as CI/CD systems that may build for numerous configurations and generators. This parsing can be trivially done in scripts in many coding languages. Here we give an example in CMake script “detect_gen.cmake”:

cmake_minimum_required(VERSION 3.21)

file(REAL_PATH ${bindir} bindir EXPAND_TILDE)

file(READ "${bindir}/CMakeCache.txt" _cache)

if(_cache MATCHES "CMAKE_GENERATOR:INTERNAL=([^ \n]+)")
  set(CMAKE_GENERATOR ${CMAKE_MATCH_1})
  message(STATUS "Detected CMake generator: ${CMAKE_GENERATOR}")
else()
  message(FATAL_ERROR "Failed to detect CMake generator")
endif()

This script is used like:

cmake -Dbindir=/path/to/build -P detect_gen.cmake

CMake rebuild cache

CMake directory property CMAKE_CONFIGURE_DEPENDS can be used to specify additional dependencies for the configuration step. For example, if a JSON file is used to specify source files, CMake wouldn’t detect if a source file was added, removed, or modified without CMAKE_CONFIGURE_DEPENDS.

Sometimes, the situation is too complicated to specify all dependencies manually. If a change is made that requires CMake to rebuild the cache, two equivalent ways to do this without modifying previously set options are:

cmake -Bbuild
# preserves prior options

or

cmake --build build -t rebuild_cache

Iterate Matlab versions with CMake

These techniques work with any versioned program or library. Here we use Matlab as an example. CMake find_package with a version range would be used to simply select from a known-working version range.

Many Matlab codes require a modern version of Matlab. It’s possible to select from an arbitrary min…max range of Matlab versions with CMake FindMatlab as follows. This technique works with other versioned programs and libraries as well.

foreach(v IN ITEMS 23.2 24.1 24.2)
  find_package(Matlab ${v} EXACT COMPONENTS MAIN_PROGRAM)
  if(Matlab_FOUND)
    add_test(NAME matlab-${v}
      COMMAND ${Matlab_MAIN_PROGRAM} -batch "buildtool"
      WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
      )
  endif()
endforeach()

Git SSL certificate location

Git typically works for HTTPS URLs using the default configuration. If certificates are not working, try configuring Git with the system SSL certificate location.

git config --global http.sslCAInfo /path/to/cert.crt

Git environment variable “GIT_SSL_CAINFO” can override this. This can be used to override missing or incorrect SSL certificate locations for Git operations in programs like Matlab.

To disable SSL certificate verification, set environment variable “GIT_SSL_NO_VERIFY” to “1”, but be aware of the security implications e.g. MITM.

CMake macOS Xcode Environment

Related: GCC / Clang header clash on macOS


When macOS, Xcode, or Command Line Tools upgrades, build directories (including for CMake-based projects) often need to be refreshed. If the user has set custom environment variables concerning Xcode, they may need to be updated as well. Here are some important environment variables and CMake variables to check if problems occur after upgrading.

For a simple CMake project on macOS, CMakeCache.txt might include:

CMAKE_OSX_DEPLOYMENT_TARGET:STRING=

CMAKE_OSX_SYSROOT:PATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX*.sdk

Having multiple directories under the following is fine:

  • /Library/Developer/CommandLineTools/SDKs
  • /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs

CMake can select the appropriate SDK by the user specifying environment variable SDKROOT or by selecting the SDK:

xcode-select --switch

The XCode versions that Homebrew targets can be found in xcode.rb.

Workarounds

If Homebrew GCC breaks after upgrading Xcode or Command Line Tools, try specifying an older SDK. For example, if g++-14 main.cpp -v shows a different (older) SDK than CMake and it works, try specifying that SDK in environment variable SDKROOT. Note that the SDK version corresponds to macOS version, not the XCode version.

export CC=gcc-14 CXX=g++-14 FC=gfortran-14

export SDKROOT=/Library/Developer/CommandLineTools/SDKs/MacOSX15.sdk/

and then source ~/gcc.sh before running cmake with a fresh build directory.

If a CMake build step fails, try copy-pasting the command and removing the -isysroot portion of the command. This is a clear clue the older SDK is (at least temporarily) needed till Homebrew updates its GCC formula.

GCC will tell where included files are coming from by adding the gcc -H flag. This tells what to specify for environment variable SDKROOT.

Example minimal projects:

Zoom audio toggle indicator

As per the Release Notes:

The Zoom app offers an option to play audio tones when muting or unmuting the microphone, providing enhanced accessibility for users with visual impairments or other disabilities. This setting is OFF by default and can be enabled from the Zoom Workplace app under Settings then Audio. When enabled, users will hear a tone, audible only to them, indicating the mute or unmute action.

CMake variables in SCRIPT role

When running CMake standalone scripts like cmake -P script.cmake this is the SCRIPT CMake role. Not all CMake information variables are set in SCRIPT role, in particular, the CMAKE_HOST* and CMAKE_SYSTEM* variables are not set as they are in PROJECT role.

This is a workaround for cmake -P SCRIPT role to get the CMAKE_HOST_* variables. It uses undocumented CMake-internal scripts, but they’ve been present since 2012 and may be unlikely to change.

message(STATUS "CMake ${CMAKE_VERSION}")

get_property(cmake_role GLOBAL PROPERTY CMAKE_ROLE)
if(cmake_role STREQUAL "SCRIPT")
  set(CMAKE_PLATFORM_INFO_DIR ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY})
  # define CMAKE_HOST*, CMAKE_SYSTEM*, etc.
  include(${CMAKE_ROOT}/Modules/CMakeDetermineSystem.cmake)
  # set booleans like CYGWIN
  include(${CMAKE_ROOT}/Modules/CMakeSystemSpecificInitialize.cmake)

  # needed by Modules/Platform/*.cmake
  include(${CMAKE_ROOT}/Modules/CMakeSystemSpecificInformation.cmake)

  # define CMAKE_SHARED_LIBRARY_SUFFIX, CMAKE_SHARED_LIBRARY_PREFIX, etc.
  include(${CMAKE_ROOT}/Modules/Platform/${CMAKE_SYSTEM_NAME}.cmake)
endif()

message(STATUS "CMAKE_SYSTEM:                ${CMAKE_SYSTEM}")
message(STATUS "CMAKE_SYSTEM_NAME:           ${CMAKE_SYSTEM_NAME}")
message(STATUS "CMAKE_SYSTEM_VERSION:        ${CMAKE_SYSTEM_VERSION}")
message(STATUS "CMAKE_SYSTEM_PROCESSOR:      ${CMAKE_SYSTEM_PROCESSOR}")
message(STATUS "CMAKE_HOST_SYSTEM:           ${CMAKE_HOST_SYSTEM}")
message(STATUS "CMAKE_HOST_SYSTEM_NAME:      ${CMAKE_HOST_SYSTEM_NAME}")
message(STATUS "CMAKE_HOST_SYSTEM_VERSION:   ${CMAKE_HOST_SYSTEM_VERSION}")
message(STATUS "CMAKE_HOST_SYSTEM_PROCESSOR: ${CMAKE_HOST_SYSTEM_PROCESSOR}")
message(STATUS "CMAKE_SHARED_LIBRARY_SUFFIX: ${CMAKE_SHARED_LIBRARY_SUFFIX}")
message(STATUS "CMAKE_SHARED_LIBRARY_PREFIX: ${CMAKE_SHARED_LIBRARY_PREFIX}")
message(STATUS "CMAKE_STATIC_LIBRARY_SUFFIX: ${CMAKE_STATIC_LIBRARY_SUFFIX}")
message(STATUS "CMAKE_STATIC_LIBRARY_PREFIX: ${CMAKE_STATIC_LIBRARY_PREFIX}")