CMake print to stdout or stderr

CMake can print to “stderr” pipe cleanly like:

message(NOTICE "this message is on stderr pipe")

However, printing cleanly without “–” leading message requires a workaround:

execute_process(COMMAND ${CMAKE_COMMAND} -E echo "this message is on stdout pipe")

NOTE: Another technique that does NOT work in general for stdout is to print the invisible carriage return character. This only works visibly on the Terminal but if piping CMake stdout to another shell command does NOT work in general.

# don't do this, only works for printed shell, doesn't work for stdout pipe

string(ASCII 13 cr)
message(STATUS "${cr}  ${cr}No dashes visible, but stdout pipe is messed up")

CMake temporary directory

Creating a unique temporary directory is not completely trivial. Some CIs and systems do not define the environment variables “TMPDIR” or “TEMP” for temporary directory. Virus scanners can false detect activity in the system temporary directory as malicious.

Create a temporary directory in CMake:

Fortran character allocatable

Variable length strings are implemented in Fortran 2003 standard like:

character(:), allocatable :: str

Passing such variables to procedures is declared the same as fixed length strings. In fact, we always declare actual arguments with “*” to avoid needing every string an exact length.

subroutine my(str)
character(*), intent(in) :: str

Note that intrinsic Fortran functions need traditional fixed length character variables. For example:

character(1000) :: buf

call get_command_argument(1, buf)

Functions can also return allocatable characters, with the caveat that some compilers like Nvidia HPC currently require the variable be manually allocated. Other compilers auto-allocate character functions like numeric arrays in Fortran 2003 code.

function greet(b)
logical, intent(in) :: b
character(:), allocatable :: greet

!! only nvfortran needs manual allocation

if(b) then
  allocate(character(5) :: greet)
  greet = 'hello'
else
  allocate(character(3) :: greet)
  greet = 'bye'
endif

end function greet

Fortran submodule file naming

In many applications, a Fortran submodule is defined in a different file than the Fortran module that uses it. Meta-build systems such as CMake and Meson must consider that each Fortran compiler treats Fortran submodule interface file naming distinctively. Fortran module interface files are like generated header files. Fortran module files are automatically generated and generally not human-readable.

A brief taxonomy of file naming conventions for Fortran submodules follows. Assume a two-file program:

file basic.f90

module demo
real, parameter :: pi = 4.*atan(1.)
real :: tau

interface
  module subroutine hello(pi,tau)
    real, intent(in) :: pi
    real, intent(out) :: tau
  end subroutine hello
end interface
contains
end module demo

program sm
use demo
call hello(pi, tau)
print *,'pi=',pi, 'tau=', tau
end program

file basic_sub.f90

submodule (demo) hi
contains
module procedure hello
  tau = 2*pi
end procedure hello
end submodule hi

The order of the commands for each compiler is significant. The module must be built before the submodule, to generate the necessary module interface files BEFORE compiling the submodule. Each compiler creates corresponding basic.o and basic_sub.o object files.

  1. gfortran -c basic.90 creates files: basic.o demo.mod demo.smod.
  2. gfortran -c basic_sub.f90 -o basic_sub.o creates files: basic_sub.o demo@hi.smod.
  3. gfortran basic.o basic_sub.o -o basic creates executable basic

Here is a table of the module interface files generated by each compiler. “module” are the files generated by step #1, building the file containing the Fortran module. “submodule” are the files generated by step #2, building the containing the Fortran submodule.

Compilermodule filessubmodule files
GCC gfortrandemo.mod demo.smoddemo@hi.smod
LLVM flangdemo.moddemo-hi.mod
Nvidia HPC nvfortrandemo.moddemo-hi.mod
Intel ifxdemo.moddemo@hi.smod
IBM xlfdemo.moddemo_hi.smod
Cray ftnDEMO.modHI.mod

Fortran submodule reference

Fortran maximum name and line lengths

Ancient Fortran code readability is impacted by the restrictions on variable length and line length that could lead to inscrutable variable and procedure names. The Fortran 2003 standard raised many of these limits to lengths that might only be a factor for auto-generated code with internally used very long names. If going beyond the usual name lengths, it’s a good idea to test across the compilers of interest (including compiler versions) to ensure that the required compiler vendors and versions can support the proposed name lengths.

We provide code examples verifying that compilers can support 63 character syntax elements (names for modules, submodules, variables), which is the maximum set by Fortran 2003 standard. The maximum line length is officially 132, but can be much longer depending on the compiler and compiler options.

CMake version recommendations and install

CMake ≥ 3.19 is strongly recommended for general users for more robust and easy syntax. For project developers, we recommend CMake ≥ 3.25 as the new features make debugging CMake and setting up CI considerably easier.

Downloading the latest release of CMake is usually easy. For Linux and Mac, admin/sudo is NOT required.

There is an unofficial PyPI CMake package:

python -m pip install cmake

For platforms where CMake binaries aren’t easily available, use build_cmake.cmake.

CMake 3.25 adds workflow presets, making configure-build-test a single command.

CMake 3.24 adds the long-awaited cmake -Bbuild --fresh option to clean the build directory first. The LINK_GROUP generator expression is excellent for resolving complex link dependencies.

CMake 3.23 further enhances –debug-find to allow debugging all find_* command, or specific find_package or find_* variables. Header files can be more cleanly installed/affiliated with targets, particularly relevant for packaging and installing with target_sources file sets.

CMake 3.22 adds several CMake Environment Variables that are generally useful. CMAKE_BUILD_TYPE default for single configuration build systems. CMAKE_CONFIGURATION_TYPES defaults available configurations for multi-config build systems like Ninja Multi-Config. CMAKE_INSTALL_MODE makes symlinks with copy fallback a good choice for installing programs from CMake. For CTest, the new ENVIRONMENT_MODIFICATION test property makes modifying environment variables for test(s) much easier.

CMake 3.21 adds more preset features, including making “generator” optional–the default CMake behavior will be used to determine generator. The cmake --install-prefix option can be used instead of cmake -DCMAKE_INSTALL_PREFIX=. PROJECT_IS_TOP_LEVEL and <PROJECT-NAME>_IS_TOP_LEVEL identify if a project is at the top of the project hierarchy. ctest --output-junit gives test output in standard tooling format.

CMake 3.20 adds support for Intel LLVM compiler and NVIDIA HPC compiler. ExternalProject_Add() learned CONFIGURE_HANDLED_BY_BUILD which avoids CMake commanding a reconfigure on each build. try_compiler(WORKING_DIRECTORY) was added. CMake presets in CMakePresets.json now covers configure, build and test, allowing many parameters to be declared with inheritance in JSON. CMake presets are a key feature for CI, as well as user configurations. ctest --test-dir build option avoids the need to manually cd build. cmake_path allows path manipulation and introspection without actually touching the filesystem.

CMake 3.19 added support for ISPC language. string(JSON GET|SET) parsing is very useful to avoid hard-coding parameters. FindPython/find_package accepts version ranges. Intel oneAPI works with CMake ≥ 3.19.6. Emits deprecation warning for cmake_minimum_required VERSION less than 2.8.12. CMakePresets.json enables configure parameter declarations in JSON.

CMake 3.18 adds CMake Profiler

cmake -B build --profiling-output=perf.json --profiling-format=google-trace

Adds REQUIRED parameter to find_*(). Adds file(ARCHIVE_CREATE) and file(ARCHIVE_EXTRACT), which is much more convenient than execute_process(COMMAND ${CMAKE_COMMAND} -E tar ${archive} WORKING_DIRECTORY ${out_dir}) syntax

CMake 3.17 adds Ninja Multi-Config generator. cmake –debug-find shows what find_*() is doing. Eliminates Windows “sh.exe is on PATH” error. Recognizes that Ninja works with Fortran.

CMake 3.16 adds precompiled headers, unity builds, many advanced project features.

CMake 3.15 adds CMAKE_GENERATOR environment variable that works like global -G option. Enhances Python interpreter finding. Adds cmake --install command instead of “cmake –build build –target install”. Added Zstd compression.

CMake 3.14: I created check_fortran_source_runs(). FetchContent was enhanced with simpler syntax. The transitive link resolution was considerably enhanced in CMake 3.14. Projects just work in CMake ≥ 3.14 that fail at link-time with CMake < 3.14.


We don’t recommend use of the older CMake versions below as they take significantly more effort to support.

CMake 3.13 adds ctest --progress and better Matlab compiler support. Lots of new linking options are added, fixes to Fortran submodule bugs. The very convenient cmake -B build incantation, target_sources() with absolute path are also added. It’s significantly more difficult to use CMake older than 3.13 with medium to large projects.

CMake 3.12 adds transitive library specification (out of same directory) and full Fortran Submodule support. get_property(_test_names DIRECTORY . TESTS) retrieves test names in current directory.

CMake 3.11 allows specify targets initially w/o sources. FetchContent is added, allowing fast hierarchies of CMake and non-CMake projects.

CMake 3.10 added Fortran Flang (LLVM) compiler and extensive MPI features.

CMake 3.9 added further C# and Cuda support, that was originally added in CMake 3.8.

CMake 3.8 added initial Cuda support

CMake 3.7 added comparing ≤ ≥ and version comparisons. Initial Fortran submodule support was added.

CMake 3.6 gave better OpenBLAS support.

CMake 3.5 enhanced FindBoost target with auto Boost prereqs.

CMake 3.4 added if(TEST) to see if a test name exists.

CMake 3.3 added list operations such as IN_LIST.

WSL cron

Windows Subsystem for Linux version 0.67.6 and newer can use systemd, which makes using Cron on WSL straightforward. Check the WSL version from Windows Terminal (from PowerShell or Command Prompt, not WSL):

wsl --version

WSL is recommended to be installed from the Microsoft Store.

Check that Cron is running in WSL:

systemctl status cron

Then as usual use crontab -e.

Cron log file

Debugging Cron jobs involves examining Cron logs. Cron logging is optional. For debugging, it may be easier to simply append to the end of the cron job line a file dump like:

$HOME/my_exe 1>/tmp/out.log 2>/tmp/err.log

On MacOS, Cron logging is disabled by default, but the results of cron jobs are available via system mail, which is stored in /var/log/$(whoami). Apple launchd can be used instead of Cron via Property List files, but many simply prefer Cron.

On Linux systems, Cron may log to locations like “/var/log/cron”.

For systems that use “rsyslog”, check “/etc/rsyslog.conf” for this line to be uncommented:

cron.*                          /var/log/cron.log

conda run non-interactive environment

“conda activate” is intended for interactive shells and therefore doesn’t work properly with Cron jobs and other non-interactive shell environments. Rather than “bash -l”, consider using conda run.

By default the stdin/stdout/stderr are captured by conda run–no text input or output is seen until after the executable finishes running. For testing scripts, it’s possible to get live stdin/stdout/stderr with the option:

conda run --no-capture-output <exe>

For example, if a Cron job runs a CTest or CDash script that needs to use a Conda Python environment “myenv”, make a Cron job like:

conda run --name myenv ctest -S memcheck.cmake

Cron jobs are single lines only in “crontab -e”. In general more complex Cron jobs are invoked by calling a script from crontab. For example suppose a CMake project using Conda Python needs to be configured, built and tested. Make a cron job script like:

# So CMake finds Conda Python environment "myenv"
conda run --name myenv \
  cmake -Bbuild

cmake --build build --parallel
# preface with "conda run --name myenv" if Python invoked during the build

conda run --name myenv \
  ctest --test-dir build

NOTE: environment variables are defined for all cron jobs at the top of the crontab. The “conda” executable must be on PATH environment variable, or specify the full path to “conda” in each cron job line.

For example, on macOS the crontab PATH might be like:

PATH=/home/username/miniconda3/condabin:/opt/homebrew/sbin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin

In particular, note that environment variables can’t refer to other environment variables like $HOME–type the full path on the system.

Gfortran type mismatch error flag

Gfortran 10 added the default behavior to make type mismatches an error instead of a warning. Legacy Fortran programs too often did not explicitly specify the procedure interfaces to allow implicit polymorphism. The Fortran 2008 standard cleaned up this situation in part with type(*).

Workaround: Gfortran flag -fallow-argument-mismatch can be used to degrade the errors to warnings. It is however strongly recommended to fix the problem in the legacy code, if it’s part of your code ownership.

Fix

For external libraries like MPI-2, the interfaces are intended to be polymorphic but use Fortran 90-style interfaces. This can be corrected from the user code by declaring an explicit interface. This specific example can be extended to other MPI-2 or other external library functions that need explicit interfaces for polymorphism.

use mpi, only : MPI_STATUS_SIZE

implicit none

interface
!! This avoids GCC >= 10 type mismatch warnings for MPI-2
subroutine mpi_send(BUF, COUNT, DATATYPE, DEST, TAG, COMM, IERROR)
type(*), dimension(..), intent(in) :: BUF
integer, intent(in) ::  COUNT, DATATYPE, DEST, TAG, COMM
integer, intent(out) :: IERROR
end subroutine

subroutine mpi_recv(BUF, COUNT, DATATYPE, SOURCE, TAG, COMM, STATUS, IERROR)
import MPI_STATUS_SIZE
type(*), dimension(..), intent(in) :: BUF
integer, intent(in) ::  COUNT, DATATYPE, SOURCE, TAG, COMM
integer, intent(out) :: STATUS(MPI_STATUS_SIZE), IERROR
end subroutine

end interface