Scientific Computing

CMake detect if project is top level

CMake can detect if a project is “top level” that is, NOT via FetchContent using PROJECT_IS_TOP_LEVEL and PROJECT_NAME_IS_TOP_LEVEL . For simplicity, we denote these variables in this article as “*_IS_TOP_LEVEL”.

Example use:

if(${PROJECT_NAME}_IS_TOP_LEVEL)
  message(STATUS "${PROJECT_NAME} directly building, not FetchContent")
endif()

For CMake < 3.21:

if(CMAKE_VERSION VERSION_LESS 3.21)
  get_property(not_top DIRECTORY PROPERTY PARENT_DIRECTORY)
  if(not_top)
    set(${PROJECT_NAME}_IS_TOP_LEVEL false)
  else()
    set(${PROJECT_NAME}_IS_TOP_LEVEL true)
  endif()
endif()

Caveats

Directory property PARENT_DIRECTORY and *_IS_TOP_LEVEL are NOT useful for detecting if the child project is being used as an ExternalProject.

These variables are based on the last “project()” command and so are not as universally useful as it first seems. For example, these variables do not work as expected when using ExternalProject. Even setting CMAKE_CACHE_ARGS of ExternalProject does not help, nor does cmake (1) command line options–the CMake-internal setting of *_IS_TOP_LEVEL overrides this attempt to set it. To workaround this, use an arbitrary auxiliary variable to detect if the project is top level.

Example:

Top-level CMakeLists.txt:

ExternalProject_Add(sub1
...
CMAKE_ARGS -DSUB1_IS_TOP:BOOL=false
)

ExternalProject_Add(sub2
...
CMAKE_ARGS -DSUB2_IS_TOP:BOOL=false
)

Subproject CMakeLists.txt

if(DEFINED SUB1_IS_TOP)
  set(SUB1_IS_TOP_LEVEL ${SUB1_IS_TOP})
endif()

Rather than try to directly workaround all the corner cases of *_IS_TOP_LEVEL, using this auxiliary variable allows the user to clearly force the intended behavior. This is useful when the subprojects and main project can build required ExternalProjects, and you want to only build the required ExternalProjects once.

GCC / Clang header clash on macOS

GCC on macOS including Homebrew-installed depends on the macOS SDK. When the macOS SDK is updated, the system headers may become incompatible with GCC versions < 13.3. Specifically, there can be syntax changes requiring C23 but that GCC < 13.3 could not handle.

Homebrew GCC 14.1 and newer work just fine, so the solution is to update GCC.

CMake 3.28, 3.29 Clang scandep workaround

CMake 3.28.0 .. 3.29.2 have a bug with Clang > 17 if CMAKE_CXX_STANDARD is set to 20 or higher before project() or enable_language(CXX). Specifically, if CMake policy CMP0155 is set to NEW by cmake_minimum_required(VERSION) or otherwise, then CMake 3.28.0 .. 3.29.2 will scan for C++ modules during initial C++ compiler checks, which is not expected or desired. To trivially workaround this issue without otherwise impacting the project or newer CMake versions, do like:

set(CMAKE_CXX_STANDARD 20)
# assuming default settings near top of CMakeLists.txt for readability

# <snip>

if(${PROJECT_NAME}_cxx)  # arbitrary user option

  set(CMAKE_CXX_SCAN_FOR_MODULES OFF)   # workaround CMake 3.28.0 .. 3.29.2 with Clang

  enable_language(CXX)

  set(CMAKE_CXX_SCAN_FOR_MODULES ON)  # optional, if project actually uses C++ modules

endif()

Related: CMake C++ standard with fallback

This issue was fixed in CMake 3.29.3.

C++ std::string with char*

C++ std::string is a dynamic, contiguous container for character strings. String data is easily and efficiently passed between std::string to / from a C or Fortran function that expects a char* pointer.

The basic algorithm is:

  1. allocate std::string with desired size and fill with \0.
  2. use std::string::data() to get a char* pointer to the string data that is read/write for the C or Fortran function (or C++).
  3. use std::string::c_str() to get a const char* pointer to the string data that is read-only for the C or Fortran function (or C++). This trims the string to the first \0 character. Otherwise, the std::string::length() will include all the unwanted trailing \0 characters.

example

CMake install via Snap

The CMake Snap package allows easy install of the latest CMake version. Scroll down to the “Install CMake on your Linux distribution” section and click on the distribution closest to the computer being used to ensure Snap is setup correctly.

After CMake install, add to PATH in ~/.profile or similar like:

export PATH=/snap/cmake/current/bin/:$PATH

C++ and C warning preprocessor directive

C++ #warning and C #warning are preprocessor directives that emit a warning message during compilation.

These trivial examples assume that a warning should be omitted if symbol “MYFEATURE” is not defined.

#ifndef MYFEATURE
#if __cplusplus >= 202302L
#  warning "C++ compiler lacks feature"
#endif
#endif
#ifndef MYFEATURE
#if __STDC_VERSION__ >= 202311L
#  warning "C compiler lacks feature"
#endif
#endif

The #if strictly check that the compiler language support is at least the specified version. Most compilers have long-supported the #warning directive without the #if check needed. That is, the following is sufficient for most compilers:

#ifndef MYFEATURE
#warning "C++ compiler lacks feature"
#endif

Related: MVSC __cplusplus macro flag

C++ std::make_unique instead of new delete

C++ std::make_unique() is a C++14 feature that creates std::unique_ptr without using new and delete. std::make_unique() is a safer and more convenient way to manage memory in C++.

GCC 4.9 circa 2014 added support for std::make_unique(), along with virtually all modern C++ compilers for several years already.

__cpp_lib_make_unique feature test macro can fence non-essential code if supporting ancient compilers is required.

CTest TEST_LAUNCHER target property

CMake target property TEST_LAUNCHER allows specifying a test launcher program. For example, MPI programs can use mpiexec to run tests with parameters. This allows deduplicating or making more programmatic test runner scripts.

Typically we create a small CMake function to configure the test launcher for each target and the associated tests.

Example

Detect CI via environment variable

CI systems typically set the environment variable CI as a de facto standard for easy CI detection. Here are details of several popular CI services:

In general, across programming languages, test frameworks allow behavior changes based on the environment variables set by the CI system.

Python Pytest

Pytest handles conditional tests well. This allows skipping a subset of tests on CI by detecting the CI environment variable:

import os
import pytest

CI = os.environ.get('CI') in ('True', 'true')


@pytest.mark.skipif(CI, reason="not a test for CI")
def test_myfun():
    ...

CMake CTest

CTest can also use CI environment variables to adjust test behavior.

if("$ENV{CI}")
  set(NO_GFX on)
endif()

add_test(NAME menu COMMAND menu)
set_tests_properties(menu PROPERTIES DISABLED $<BOOL:${NO_GFX}>)

Always enclose "$ENV{}" in quotes in case the environment variable is empty or not defined, or the configuration step may syntax error.