CMake Crash Course

Edoardo Pasca & Alin M Elena

ukri stfc Scientific Computing

February 2025

Introduction

What is CMake?

  • CMake is a cross-platform, open source build system generator supporting different build tools:
    • Visual Studio Solutions
    • GNU MakeFiles
    • etc

Program of the Crash Course

  • clone the repository https://gitlab.com/drFaustroll/cmake-tutorial
  • Single C++/Fortran file to build to executable
  • typical workflow with CMake
  • source directory, build directory, install directory
  • We will implement dot product and will add OpenMP
  • We will use BLAS and cuBLAS
  • More advanced topics if time permits

How does CMake work?

  • CMake reads text files named CMakeLists.txt in the source tree.
  • such files describe what you want to do (see later)
  • CMake generates the Visual Studio Solution and/or Makefiles for you

Example 1: Hello World!

Concepts 1

  • Generator CMAKE_GENERATOR
  • Source directory CMAKE_SOURCE_DIR
  • Build directory CMAKE_BINARY_DIR
  • Install directory CMAKE_INSTALL_PREFIX

Typical workflow

mkdir build
cd build
cmake ..
cmake --build .
cmake --build . --target install

or in one line
cmake -S 01-minimal/cxx -Bbuild-01
cmake --build build-01

Example 1 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(hello LANGUAGES CXX)

add_executable(hello.x src/hello.cxx)
project(hello LANGUAGES Fortran)

add_executable(hello src/hello.F90)

Example 2

Running example

Leibnitz formula for \(\pi\)

\[\frac{\pi}{4} = 1 - \frac{1}{3} + \frac{1}{5} - \frac{1}{7} + \frac{1}{9} - \frac{1}{11} + \dots\]

Dot product

double dot(const double* A, const double* B, const int n){

   double s=0.0;
   for(int i=0; i<n; ++i) {
     s += A[i]*B[i];
   }
   return s;
 }

Project structure

cxx/CMakeLists.txt
cxx/include/algebra.h
cxx/src/algebra.cxx
cxx/src/testdot.cxx

02.1

cmake_minimum_required(VERSION 3.10)
cmake_policy(SET CMP0048 NEW)

project(testdot LANGUAGES CXX VERSION 0.1.0.1)
set(AUTHOR "Alin Elena;Edoardo Pasca")
message(STATUS "building ${PROJECT_NAME},
                version ${PROJECT_VERSION}")
set(CMAKE_CXX_STANDARD 11)
add_executable(testdot
     src/algebra.cxx
     src/testdot.cxx
     )
target_include_directories(testdot PRIVATE
                           ${PROJECT_SOURCE_DIR}/include)

02.2 include and add_subdirectory

include(GNUInstallDirs)

set(LIBRARY_OUTPUT_PATH
            ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(EXECUTABLE_OUTPUT_PATH
            ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})

add_subdirectory(src)

02.2 static library

add_library(algebra STATIC algebra.cxx)
add_executable(testdot testdot.cxx )
target_include_directories(algebra PUBLIC
                           "${PROJECT_SOURCE_DIR}/include")
target_link_libraries(testdot algebra)

Intermezzo CMake Language

02.3 Adding Build Options

option(BUILD_DOCS "Build with API Docs" OFF)
option(BUILD_SHARED_LIBS "Build shared libraries" ON)

if(BUILD_DOCS)
  find_package(Doxygen REQUIRED)
  configure_file(${PROJECT_SOURCE_DIR}/cmake/Doxyfile.cmake ${PROJECT_BINARY_DIR}/Doxyfile)
  add_custom_target(docs
    ${DOXYGEN_EXECUTABLE} ${PROJECT_BINARY_DIR}/Doxyfile)
endif()

02.3 continue

cmake -DBUILD_DOCS=On|Off

02.3 Install directory

install(TARGETS algebra testdot
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  )
install(FILES ${PROJECT_SOURCE_DIR}/include/algebra.h
  DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

02.4 Packaging

set(CPACK_GENERATOR "RPM;DEB")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "John Smith")
include(CPack)

02.4 Packaging Windows

set(CPACK_GENERATOR NSIS)
cmake --build . --config Release

02.4 Shared library

if (${BUILD_SHARED_LIBS})
  add_library(algebra SHARED algebra.cxx)
  if (WIN)
    set (FLAGS "/Ddll_EXPORTS")
  endif()
  set_target_properties(algebra PROPERTIES
    VERSION ${testdot_VERSION}
    SOVERSION ${testdot_VERSION_MAJOR})
else()
  add_library(algebra STATIC algebra.cxx)
endif()

02.5 OpenMP

include(FindOpenMP)
if(OpenMP_CXX_FOUND)
  message(STATUS
  "OpenMP for C++ Compiler Found, version ${OpenMP_CXX_VERSION_MAJOR}
  .${OpenMP_CXX_VERSION_MINOR}")
else()
  message(ERROR_CRITICAL "No OpenMP support detected")
endif()

02.5 OpenMP

double dot(const double* A, const double* B, const int n){

double s=0.0;

#pragma omp parallel for default(none) shared(A,B) reduction(+:s)
  for(int i=0; i<n; ++i) {
    s += A[i]*B[i];
  }
  return s;
}

CMake Language

Organisation

CMake Language source files in a project are organized into:

  • Directories (CMakeLists.txt),
  • Scripts (<script>.cmake) tipically run as cmake -P <script>.cmake
  • Modules (<module>.cmake) tipically used with the include() command to load them in the scope of the including context

Encoding

CMake source code is saved in text files encoded as 7bit ASCII for max portability.

UTF-8 is OK.

Command invocation

A command invocation is a name followed by paren-enclosed arguments separated by whitespace

add_executable(hello.x src/hello.cxx)

Command names are case-insensitive.

Nested unquoted parentheses in the arguments must balance.

Unquoted command arguments

It may not contain any whitespace, ( ) # \ " except when escaped by a backslash

install(TARGETS algebra testdot
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  )

Quoted command arguments

Content is enclosed between opening and closing ” double-quote .

Both Escape Sequences and Variable References are evaluated.

A quoted argument is one argument for the command.

message(STATUS
  "OpenMP for C++ Compiler Found,
  version ${OpenMP_CXX_VERSION_MAJOR}.
  ${OpenMP_CXX_VERSION_MINOR}")

Bracket command arguments

Content is enclosed between opening and closing brackets of the same length

Evaluation of the enclosed content is not performed, such as Escape Sequences or Variable References.

message([=[
This is the first line in a bracket argument with bracket length 1.
No \-escape sequences or ${variable} references are evaluated.
This is always one argument even though it contains a ; character.
The text does not end on a closing bracket of length 0 like ]].
It does end in a closing bracket of length 1.
]=])

Variables

  • are always of string type
  • A variable reference has the form ${<variable>}
  • Variable references can nest and are evaluated from the inside out, ${outer_${inner_variable}_variable}
  • Scope: Function, Directory, Cache
  • environement variables have the form $ENV{<variable>}

Lists

A list of elements is represented as a string by concatenating the elements separated by ;.

set(srcs a.c b.c c.c)
# sets "srcs" to "a.c;b.c;c.c"

CMake Cache

The cache is best thought of as a configuration file.

The first time CMake is run, it produces a CMakeCache.txt file which contains entries added in response to certain CMake commands as find_package

After CMake has been run, and created CMakeCache.txt – you may edit it.

if control loop

if(somevar)
  do this
elseif(someother)
  do that
else()
  do something else
endif()

variable references can be in the short form <variable> instead of ${<variable>}. Not valid for environment and cache variables.

foreach control loop

Evaluate a group of commands for each value in a list.

set(A 0;1)
set(B 2 3)
set(C "4 5")
set(D 6;7 8)
set(E "")
foreach(X IN LISTS A B C D E)
    message(STATUS "X=${X}")
endforeach()

What does it return?

while control loop

Evaluate a group of commands while a condition is true

while (<condition>)
  do something
endwhile()

Function and Macro

Are pieces of code for later execution.

macro(<name> [<arg1> ...])
  <commands>
endmacro()
function(<name> [<arg1> ...])
  <commands>
endfunction()

Function and Macro

In a function, ARGN, ARGC, ARGV and ARGV0, ARGV1, … are true variables in the usual CMake sense. In a macro, they are not, they are string replacements which makes normal CMake syntax cumbersome.

A macro is executed as if the macro body were pasted in place of the calling statement.

Back to 02.3 Adding Build Options

03

03.1 Compiler feature selection

cmake_minimum_required(VERSION 3.10)
message(STATUS "CXX_COMPILE_FEATURES ${CMAKE_CXX_COMPILE_FEATURES}")

target_compile_features(algebra PRIVATE cxx_std_11)

03.2 BLAS conditional compiling external libraries

find_package(PkgConfig REQUIRED)
pkg_check_modules(openblas REQUIRED openblas>=0.3)
set(BLAS_LIBRARIES ${openblas_LIBRARIES})
set(BLAS_INCLUDEDIR ${openblas_INCLUDEDIR})

03.2 BLAS conditional compiling external libraries

if (ENABLE_BLAS)
  set(BLA_VENDOR OpenBLAS)
  find_package(BLAS REQUIRED)
endif()

03.2 notes on BLAS 1

if pkg-config is installed by conda then one needs to set the path to it

export PKG_CONFIG_PATH=${CONDA_PREFIX}/lib/pkgconfig:$PKG_CONFIG_PATH

03.2 notes on BLAS 2

find_package(BLAS)

requires BLAS library and includes to be available in default locations. In Linux this means you need to add:

  1. the path to the BLAS library to LD_LIBRARY_PATH
  2. the path to the BLAS includes to CPATH or C_INCLUDE_PATH and CPLUS_INCLUDE_PATH

03.3 Build testing

option(BUILD_TESTING "Build with tests" ON)
if (BUILD_TESTING)
  include(CTest)
  #testing this macro in big projects may go in its own file
  macro (do_test testname n ns result)
    add_test(${testname} ${CMAKE_INSTALL_BINDIR}/testdot ${n} ${ns})
    set_tests_properties (${testname}
      PROPERTIES PASS_REGULAR_EXPRESSION ${result})
  endmacro (do_test)

  do_test(test1 10 10 "Last value of pi: 3.19419")
  add_test(NAME run_unittest COMMAND unittest)

endif()

03.4 External Project

include(ExternalProject)
ExternalProject_Add(algebra
  GIT_REPOSITORY https://gitlab.com/drFaustroll/algebra_cxx.git
  GIT_TAG v0.1.0.1
  GIT_SHALLOW true
  GIT_PROGRESS true
  PREFIX external-algebra
  BINARY_DIR external-algebra/algebra-build
  SOURCE_DIR external-algebra/algebra
  CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${PROJECT_BINARY_DIR}/external
      -DBUILD_TESTING=off -DBUILD_SHARED_LIBS=off
)

04 Multiple Languages

04.1 CXX and Fortran

project(hello LANGUAGES CXX Fortran)

add_executable(helloc.x src/hello.cxx)
add_executable(hellof.x src/hello.F90)

04.2 CUDA

enable_language(CUDA)
add_executable(cudadottest ${CMAKE_CURRENT_SOURCE_DIR}/testdot.cu)

if (CMAKE_VERSION VERSION_LESS "3.17")
  find_package(CUDA REQUIRED)
  set (dottest_link_libraries ${CUDA_CUBLAS_LIBRARIES})
else()
  find_package(CUDAToolkit REQUIRED)
  set (dottest_link_libraries CUDA::cublas)
endif()
add_definitions(-DCUDABLAS)
target_link_libraries(cudadottest ${dottest_link_libraries})

all CMake

cmake -B build-mycode -S mysource -DCMAKE_INSTALL_PREFIX=$HOME/mycodes
cmake --build build-mycode --target install
cmake -E --help

do it yourself

time to convert a project to CMake

  • C++ example in code/05-doItYourself/cxx needs external library boost…
  • Fortran example code/05-doItYourself/Fortran needs no external library