Brief tour to demonstrate concepts/capabilities

Objectives

  • Get a feel for how CMake looks.

  • See 2-3 use cases where CMake shines (environment/library discovery, code generation, testing, modular code structure, Fortran 90 support).

  • The goal here is not to focus on syntax or details but rather the big picture and the potential.

Note

To follow-along in your terminal you can clone or download the tutorial repository (https://github.com/coderefinery/cmake-workshop):

$ git clone https://github.com/coderefinery/cmake-workshop.git

You find the example in the content/examples/calculator/ folder.

We will discuss an example project “calculator” which can add and subtract and is a mixed Fortran-C++ project with the following layout:

calculator/
├── CMakeLists.txt
├── src
│   ├── add.f90
│   ├── calculator.h
│   ├── CMakeLists.txt
│   ├── git-version.cmake
│   ├── main.cpp
│   ├── subtract.f90
│   └── version-template.h
└── test
    ├── calculator.cpp
    ├── CMakeLists.txt
    └── driver.cpp
  1. Before we have a closer look we open up the top-level CMakeLists.txt and for the moment comment out the part with the testing. We will bring it back later:

     1# stop if CMake version is below 3.0
     2cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
     3
     4# project name and supported languages
     5project(example VERSION 1.0.0 LANGUAGES CXX Fortran)
     6
     7# require C++11
     8set(CMAKE_CXX_STANDARD 11)
     9set(CMAKE_CXX_EXTENSIONS OFF)
    10set(CMAKE_CXX_STANDARD_REQUIRED ON)
    11
    12# specify where to place binaries and libraries
    13# so they end up in standard places with "make install"
    14include(GNUInstallDirs)
    15set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
    16set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
    17set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
    18
    19# process src/CMakeLists.txt
    20add_subdirectory(src)
    21
    22# enable testing
    23enable_testing()
    24
    25# process test/CMakeLists.txt
    26add_subdirectory(test)
    
  2. Now we build the project and observe what it means to build out of source.

    $ cmake -S. -Bbuild
    $ cmake --build build
    

    But many people (especially CMake old-timers like the presenter) do this instead with Unix Makefiles:

    $ mkdir build
    $ cd build
    $ cmake ..
    $ make
    
  3. We also run the compiled binary:

    $ ./bin/calculator.x
    
    Running calculator version v1.0.0
    Configure-time Git hash: fca3425
    
    2 + 3 = 5
    2 - 3 = -1
    
  4. We browse and discuss the main CMakeLists.txt and also src/CMakeLists.txt.

  5. We discuss how the Git hash ends up in the generated version.h with the help of:

     1# in case Git is not available, we default to "unknown"
     2set(GIT_HASH "unknown")
     3
     4# find Git and if available set GIT_HASH variable
     5find_package(Git QUIET)
     6
     7if(GIT_FOUND)
     8  execute_process(
     9    COMMAND ${GIT_EXECUTABLE} --no-pager show -s --pretty=format:%h -n 1
    10    OUTPUT_VARIABLE GIT_HASH
    11    OUTPUT_STRIP_TRAILING_WHITESPACE
    12    WORKING_DIRECTORY
    13      ${PROJECT_SOURCE_DIR}
    14    ERROR_QUIET
    15    )
    16endif()
    
  6. Then we re-activate the testing, reconfigure from scratch (this is not needed but it shows the moment better when the external library is downloaded):

     1# enable FetchContent
     2include(FetchContent)
     3
     4# declare where to find the dependency
     5FetchContent_Declare(GoogleTest
     6  GIT_REPOSITORY https://github.com/google/googletest.git
     7  GIT_TAG        release-1.11.0
     8  )
     9
    10# bring the dependency into scope
    11FetchContent_MakeAvailable(GoogleTest)
    12
    13# build and link the unit_tests executable
    14add_executable(unit_tests)
    15target_sources(
    16  unit_tests
    17  PRIVATE
    18    calculator.cpp
    19    driver.cpp
    20  )
    21target_link_libraries(
    22  unit_tests
    23  PRIVATE
    24    calculator
    25    gtest_main  # this target is not here, we fetch it from the web (above)
    26  )
    27
    28# define tests
    29add_test(
    30  NAME adding
    31  COMMAND $<TARGET_FILE:unit_tests> --gtest_filter=calculator.add
    32  )
    33add_test(
    34  NAME subtracting
    35  COMMAND $<TARGET_FILE:unit_tests> --gtest_filter=calculator.subtract
    36  )
    
  7. We build and run the test set:

    $ ctest
    
  8. We also run the test set (consisting of two tests) in parallel:

    $ ctest -j 2
    
  9. For the Fortran folks we create one more Fortran module and “use” it in add.f90 and demonstrate that the order does not matter and CMake figures it out.

  10. We try the install target:

    $ mkdir build
    $ cd build
    $ cmake -D CMAKE_INSTALL_PREFIX=/tmp/calculator-example
    $ make
    $ make test
    $ make install