How to Modern CMake

Modern CMake is all about targets and properties. The whole point is to avoid using CMake plain variables and apply the same best practices we apply to our source code. Using the targets and properties approach comes packed with additional machinery that makes identifying errors and exporting projects way easier.

An additional argument to stop using variables in CMake is that plain variables are just replaced as strings which are error prone. For example, if we commit a typo, the variable is just translated into a an empty string, which might be quite difficult to debug. Working with this new CMake approach is really easy.

First we define our targets. There are to options:

add_executable
add_library

I will use as example a new project I’m working on: a C++ chip8 interpreter. The project structure looks like this:

build
cmake
depends
docs
include
src/
    main.cpp
    other source files
test/
    test source files
CMakelists.txt
README.md

We basically have 4 targets in this project. The main executable, the rest of the sources (which I refer to as the core library), the test executable, and the test case sources.

This division allows us to easily tune the properties of each target appropriately. For example, we will use different compile flags for test and the core files.

In the cmake folder we will store the additional required files:

cmake/
    chip8_core_lib.cmake
    chip8_test_lib.cmake
    scripts/
        functions and macros common among the project

Our project cmake files should be prefixed with the project name, which will act as a namespace and will reduce the risk of collision with other projects’ scripts.

chip8_core_lib.cmake will define the core lib target. We will use an object library, which will compile the files but not link them.

add_library(CHIP8_CORE_LIB
    OBJECT
        src/Interpreter.cpp
        src/io/Speaker.cpp
        src/io/Keypad.cpp
        src/io/KeyMap.cpp
        src/io/display/PixelArray.cpp
        src/io/display/Renderer.cpp
        src/details/memory.cpp
        src/details/audio.cpp
        src/registers/DataRegister.cpp
        src/registers/IRegister.cpp
)

target_include_directories(CHIP8_CORE_LIB 
    PUBLIC 
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_PREFIX}/include>
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    PRIVATE 
        ${CMAKE_CURRENT_SOURCE_DIR}/src
    )

target_compile_features(CHIP8_CORE_LIB PRIVATE cxx_std_17)

And our test object library:

add_library(CHIP8_TESTS_LIB
    OBJECT
        test/rom_test.cpp
        test/timer_test.cpp
        test/speaker_test.cpp
        test/keypad_test.cpp
        test/display_test.cpp
        test/register_test.cpp
        test/random_test.cpp
)

target_include_directories(CHIP8_TESTS_LIB PRIVATE include)

target_compile_features(CHIP8_TESTS_LIB PRIVATE cxx_std_17)

Our main CMake file can make use of these two object libraries to build the executable targets:

cmake_minimum_required(VERSION 3.1 FATAL_ERROR)
project(chip8_cpp VERSION 1.0.0)

list(APPEND CMAKE_MODULE_PATH 
    "${PROJECT_SOURCE_DIR}/cmake"
    "${PROJECT_SOURCE_DIR}/cmake/modules"
    "${PROJECT_SOURCE_DIR}/cmake/scripts"    
)

set(MAIN_EXECUTABLE ${PROJECT_NAME}_interpreter)

include(chip8_core_lib)
add_executable(${MAIN_EXECUTABLE} src/main.cpp  
    $<TARGET_OBJECTS:CHIP8_CORE_LIB>
)

if(BUILD_TESTS)
    enable_testing()

    set(TESTS_EXECUTABLE ${PROJECT_NAME}_tests)

    include(chip8_tests_lib)
    add_executable(${TESTS_EXECUTABLE} test/test_main.cpp 
        $<TARGET_OBJECTS:CHIP8_CORE_LIB> 
        $<TARGET_OBJECTS:CHIP8_TESTS_LIB>
    )
endif()

This structure allows as to have full control over the targets. Let’s say we want to add clang-tidy and set different flags for the different targets. We will be using this for all targets, so lets create a script:

function(target_add_clang_tidy TARGET_NAME_ARG CLANG_TIDY_CHECKS)

    if(${CLANG_TIDY})
        find_program(
            CLANG_TIDY_PATH
            NAMES "clang-tidy"
            DOC "Path to clang-tidy executable"
            )

        if(NOT CLANG_TIDY_PATH)
            message(FATAL_ERROR "Clang-tidy not found.")
        endif()
        
        execute_process (
            COMMAND ${CLANG_TIDY_PATH} --version
            OUTPUT_VARIABLE CLANG_TIDY_VERSION
        )

        # if clang-tidy version > 8.0.0
        if(${CLANG_TIDY_VERSION} MATCHES "([1-9]+[0-9]|[8-9])\.([0-9]+)\.([0-9]+)")
            message(STATUS "Clang-tidy found: ${CLANG_TIDY_PATH}")
            set(CLANG_TIDY_PATH_AND_OPTIONS "${CLANG_TIDY_PATH}" "${CLANG_TIDY_CHECKS}")

            set_target_properties(
                ${TARGET_NAME_ARG} PROPERTIES
                CXX_CLANG_TIDY "${CLANG_TIDY_PATH_AND_OPTIONS}"
                )
        else()
            message(FATAL_ERROR 
                "Minimum clang-tidy version is 8.0.0\n
                Current version:\n ${CLANG_TIDY_VERSION}\n
                Please upgrade."
            )
        endif()
    endif()

endfunction(target_add_clang_tidy)

Now we can set clang tidy flags for our core libs, and different ones for the test files:

include(chip8_clang_tidy)
string(CONCAT CHIP8_CORE_CLANG_TIDY_CHECKS "-checks=*,"
    # Disabled checks must be marked with a slash prefix
    "-fuchsia-*" "," 
    "-google-readability-namespace-comments" ","
    "-google-readability-todo" ","
    "-llvm-namespace-comment" ","
    "-hicpp-uppercase-literal-suffix" ","
    "-readability-uppercase-literal-suffix"
    )
target_add_clang_tidy(CHIP8_CORE_LIB ${CHIP8_CORE_CLANG_TIDY_CHECKS})
include(chip8_clang_tidy)
string(CONCAT CHIP8_TESTS_LIB_CLANG_TIDY_CHECKS "-checks=*,"
    # Disabled checks must be marked with a slash prefix
    "-fuchsia-*" ","
    "-google-readability-namespace-comments" ","
    "-google-readability-todo" ","
    "-llvm-namespace-comment" ","
    "-hicpp-uppercase-literal-suffix" ","
    "-readability-uppercase-literal-suffix" ","
    "-cppcoreguidelines-special-member-functions" ","
    "-hicpp-special-member-functions" ","
    "-cert-err58-cpp" ","
    "-cppcoreguidelines-owning-memory" ","
    "-cppcoreguidelines-avoid-goto" ","
    "-hicpp-avoid-goto" ","
    "-cppcoreguidelines-avoid-magic-numbers" ","
    "-readability-magic-numbers" ","
    "-cppcoreguidelines-pro-type-vararg" ","
    "-hicpp-vararg"
    )
target_add_clang_tidy(CHIP8_TESTS_LIB ${CHIP8_TESTS_LIB_CLANG_TIDY_CHECKS})

That’s just an example of what we can easily do! If you are struggling grasping all this concepts, do check the magnificent talk from Daniel Pfeifer:

This Post Has 6 Comments

  1. Justin

    Long time supporter, and thought I’d drop a comment.

    Your wordpress site is very sleek – hope you don’t mind
    me asking what theme you’re using? (and don’t mind if I steal it?
    :P)

    I just launched my site –also built in wordpress like yours– but the theme slows (!) the
    site down quite a bit.

    In case you have a minute, you can find it by searching for “royal cbd” on Google (would
    appreciate any feedback) – it’s still in the works.

    Keep up the good work– and hope you all take care of yourself during
    the coronavirus scare!

    1. admin

      Hi, Justin thank you for your support! The theme is OceanWP Theme by Nick. Have a good one!

  2. Dorothy

    My spouse and I stumbled over here from a different web address
    and thought I should check things out. I like what I see so now i’m following you.
    Look forward to looking over your web page for a
    second time.

  3. Marilyn

    Hey there! Would you mind if I share your blog with my myspace group?

    There’s a lot of people that I think would really appreciate your
    content. Please let me know. Cheers

  4. Brenda

    Wow, amazing blog layout! How long have you been blogging for?

    you made blogging look easy. The overall look of your web site is magnificent, let alone the content!

  5. Ann

    wonderful points altogether, you simply won a new reader.
    What might you suggest about your submit that you just made
    some days ago? Any certain?

Leave a Reply