CMake

Page Contents

References / useful Links

  • https://github.com/onqtam/awesome-cmake
  • https://www.jetbrains.com/help/clion/quick-cmake-tutorial.html
  • https://cgold.readthedocs.io/en/latest/overview.html
  • https://github.com/toeb/moderncmake/blob/master/Modern%20CMake.pdf
  • https://stackoverflow.com/questions/8934295/add-source-in-a-subdirectory-to-a-cmake-project
  • https://stackoverflow.com/questions/17653738/recursive-cmake-search-for-header-and-source-files
  • https://github.com/ruslo/sugar/wiki/Collecting-sources
  • https://crascit.com/2015/02/02/cmake-target-dependencies/
  • http://floooh.github.io/2016/01/12/cmake-dependency-juggling.html
  • http://crascit.com/2016/01/31/enhanced-source-file-handling-with-target_sources/
  • https://cmake.org/pipermail/cmake/2016-May/063400.html
http://derekmolloy.ie/hello-world-introductions-to-cmake/
https://mirkokiefer.com/cmake-by-example-f95eb47d45b1
https://www.udemy.com/introduction-to-cmake/
https://cgold.readthedocs.io/en/latest/overview/cmake-can.html
https://crascit.com/professional-cmake/
https://pabloariasal.github.io/2018/02/19/its-time-to-do-cmake-right/

Install From Source

sudo apt install build-essential checkinstall zlib1g-dev libssl-dev
wget https://github.com/Kitware/CMake/archive/refs/tags/v3.28.0-rc4.tar.gz
tar -zxvf v3.28.0-rc4.tar.gz
( cd CMake-3.28.0-rc4
    ./bootstrap && make && sudo make install
)
rm -fr CMake-3.28.0-rc4 

Intro

Some commonly used commands:

  • message: prints given message.
  • cmake_minimum_required: sets minimum version of cmake to be used.
  • add_executable: adds executable target with given name.
  • add_library: adds a library target to be build from listed source files.
  • add_subdirectory: adds a subdirectory to build. CMake will enter this directory, find the CMakeLists.txt and build using that.

Build And Use A Library

In your main CMakeLists.txt add:

add_subdirectory(...)
...            
target_link_libraries(project-name library-name)

In your library subdirectory add another CMakeLists.txt:

cmake_minimum_required(VERSION ...)
set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/lib)
add_library(library-name SHARED|STATIC ...sources...)

Functions And Macros

Functions introduce new scope, macros do not - they're a little like a C preprocessor macro. Neither can return in the sense that a normally programming language returns a value. Returning is done via an output parameter.

Generically:

function(function-name arg1 arg2 ...)
    set(function-name-output-parameter ... PARENT_SCOPE)
endfunction()

macro(macro-name arg1 arg2)
    set(macro-name-output-parameter ...)    
endmacro()

Function arguments that are specified in the definition are required, but the function can accept any number of parameters using automatically and scoped vairables ARGV, ARGC, ARGn, where n is the index of the argument. See also cmake_parse_arguments().

Embedded C Project Template

# Project Configuration
cmake_minimum_required(VERSION 3.16)

# Note, project type set to 'NONE' deliberately so that CMake does not search for C compiler before the toolchain
# is configured.
project(my-project-name NONE)

set(CMAKE_VERBOSE_MAKEFILE ON)

# Toolchain
set(CMAKE_SYSTEM_NAME      "Generic")
set(CMAKE_SYSTEM_PROCESSOR "arm")
set(CMAKE_CROSSCOMPILING   true)

# Skip link step during toolchain validation.
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

set(TOOLCHAIN_TUPLE "arm-none-eabi")
find_program(CMAKE_C_COMPILER   "${TOOLCHAIN_TUPLE}-gcc")
find_program(CMAKE_ASM_COMPILER "${CMAKE_C_COMPILER}")
find_program(CMAKE_OBJCOPY      "${TOOLCHAIN_TUPLE}-objcopy")
find_program(CMAKE_OBJDUMP      "${TOOLCHAIN_TUPLE}-objdump")
find_program(CMAKE_SIZE         "${TOOLCHAIN_TUPLE}-size")

# Search only under *target* paths.
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

# Programming Languages - C and Assembly
enable_language(C ASM)

# Set the suffix for executables on this platform.
set (CMAKE_EXECUTABLE_SUFFIX ".elf")

# Target Parameters
set(TARGET_MCU       "STM32...")
set(TARGET_FAMILY    "STM32...")

# Target Compiler/Linker Flags
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE "Debug")
    message(STATUS "Build type not specified, defaulting to 'Debug'")
endif()

# See https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
set(TARGET_WARNING_FLAGS
        "-Werror"                   # Make all warnings into errors.
        "-Wall"                     # Enable all the warnings about constructions that some users consider questionable, and that are easy to avoid etc. (See GCC docs for list).
        "-Wextra"                   # Enable some extra warning flags not enabled by -Wall. (See GCC docs for list).
        "-Wshadow"                  # Warn whenever a local variable or type declaration shadows another variable, parameter, type etc.
        "-Wcast-align=strict"       # Warn whenever a pointer is cast such that the required alignment of the target is increased regardless of the target machine.
        "-Wpointer-arith"           # Warn about anything that depends on the “size of” a function type or of void.
        "-pedantic"                 # Issue all the warnings demanded by strict ISO C.
        "-Wconversion"              # Warn for implicit conversions that may alter a value.
        "-Wsign-conversion"         # Warn for implicit conversions that may change the sign of an integer value.
        "-Wfloat-conversion"        # Warn for implicit conversions that reduce the precision of a real value.
        "-Wfloat-equal"             # Warn when floating-point values are used in equality expressions.
        "-Wcast-qual"               # Warn whenever a pointer is cast so as to remove a type qualifier from the target type.
        "-Wduplicated-branches"     # Warn when an if-else has identical branches.
        "-Wduplicated-cond"         # Warn about duplicated conditions in an if-else-if chain.
        "-Wswitch-default"          # Warn whenever a switch statement does not have a default case.
        "-Wwrite-strings"           # When compiling C, give string constants the type const char[length] so that copying the address of one into a non-const char * pointer produces a warning.
        "-Wunused-macros"           # Warn about macros defined in the main file that are unused.
)
if ("${DEBUG_OUTPUT_METHOD}" STREQUAL "RTT")
    # The RTT libraries generate the following warning: supress it when using RTT.
    list(APPEND TARGET_WARNING_FLAGS -Wno-cast-align)
endif()
STRING(REPLACE ";" " " TARGET_WARNING_FLAGS "${TARGET_WARNING_FLAGS}")

# Common flags and defines - things like "-mcpu=xxx", "-mlittle-endian", "-mthumb" etc
set(TARGET_COMMON_FLAGS   ...flags appropriate to your target...
)
STRING(REPLACE ";" " " TARGET_COMMON_FLAGS "${TARGET_COMMON_FLAGS}")

set(TARGET_DEFINITIONS
    "-DCORE_CM..."
    "-D${TARGET_FAMILY}"
    "-DEVAL_BOARD"
)
STRING(REPLACE ";" " " TARGET_DEFINITIONS "${TARGET_DEFINITIONS}")

# Segger RTT
# Add `-DDEBUG_OUTPUT_METHOD=RTT` to CMake options in CMake profile to use.
if ("${DEBUG_OUTPUT_METHOD}" STREQUAL "RTT")
    message(STATUS "Using SEGGER RTT protocol for debug output")
    include_directories("${CMAKE_SOURCE_DIR}/path-to-segger-rtt-source-files")
endif ()

set(TARGET_COMPILER_FLAGS "-ffunction-sections -fdata-sections")
set(TARGET_C_FLAGS "-std=c11")
set(TARGET_LD_FLAGS "-Wl,--gc-sections")

# Set language-wide flags for C language, used when building for all configurations.
set(CMAKE_C_FLAGS "${TARGET_WARNING_FLAGS} ${TARGET_COMMON_FLAGS} ${TARGET_DEFINITIONS} ${TARGET_COMPILER_FLAGS} ${TARGET_C_FLAGS}")

# Set linker flags to be used to create executables. These flags will be used by the linker when creating an executable.
set(CMAKE_EXE_LINKER_FLAGS "${TARGET_COMMON_FLAGS} ${TARGET_LD_FLAGS}")

# Flags for Debug build type or configuration.
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS} -DTARGET_DEBUG  -DAPP_DEBUG -Og -ggdb")
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${TARGET_COMMON_FLAGS} ${TARGET_LD_FLAGS}")

# Flags for Release build type or configuration.
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS} -Os -DNDEBUG")
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${TARGET_COMMON_FLAGS} ${TARGET_LD_FLAGS}")

# Project Layout
set(CMAKE_INCLUDE_CURRENT_DIR true)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")

include_directories("${CMAKE_SOURCE_DIR}/...your include dirs ...")

# Set output directory into which runtime target files should be built.
SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})

# Add sources to executable target...
# CMake recommends not using globbing and listing explicitly. With globbing it warns that you can compile files that
# you don't mean to and that the addition/removal of a file is not present in your build system diff, so tracking down
# "what did you change?" in debugging reported problems can be harder since there's no evidence of accidentally
# added/removed files in a normal Git diff output.
add_executable(application
    // List c files
)

if ("${DEBUG_OUTPUT_METHOD}" STREQUAL "RTT")
    # Only add SEGGER RTT sources if compiling with RTT enabled
    target_sources(application
            PUBLIC "${CMAKE_SOURCE_DIR}/path-to-segger/SEGGER_RTT.c"
    )
endif ()

# Set linker flags.
set(NI_LINK_FLAGS
        -T'${CMAKE_CURRENT_SOURCE_DIR}/path-to-LD-file'
        -mcpu=cortex-...
        -mthumb
        -Wl,-gc-sections,--print-memory-usage,-Map=${PROJECT_BINARY_DIR}/${PROJECT_NAME}.map
)
STRING(REPLACE ";" " " NI_LINK_FLAGS "${NI_LINK_FLAGS}")
set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "${NI_LINK_FLAGS}")

# Build HEX and BIN files from the ELF target
set(HEX_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.hex)
set(BIN_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.bin)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
        COMMAND ${CMAKE_OBJCOPY} -Oihex $lt;TARGET_FILE:${PROJECT_NAME}> ${HEX_FILE}
        COMMAND ${CMAKE_OBJCOPY} -Obinary $$lt;TARGET_FILE:${PROJECT_NAME}> ${BIN_FILE}
        COMMENT "Building ${HEX_FILE}
Building ${BIN_FILE}")
    

TARGET_COMMON_FLAGS For Cortex M0

# The target is an ARM Cortex-M0, which implies the thumb instruction set. It is little endian and has no
# hardware FP support.
set(TARGET_COMMON_FLAGS   "-mlittle-endian"
                            "-mcpu=cortex-m0"
                            "-mthumb"
                            "-mfloat-abi=soft"
)
STRING(REPLACE ";" " " TARGET_COMMON_FLAGS "${TARGET_COMMON_FLAGS}")    
        

One thing I did want to do was to check my GCC compiler version. CMake should support macros to do this (CMAKE_CXX_COMPILER_ID and CMAKE_CXX_COMPILER_VERSION) but they didn't work for me so I ended up writing the following:

if (CMAKE_C_COMPILER STREQUAL "CMAKE_C_COMPILER-NOTFOUND")
            message(FATAL_ERROR "Compiler not found")
        else()
            execute_process(
                    COMMAND "${CMAKE_C_COMPILER}" "--version"
                    COMMAND "grep" "-Eo" "[0-9]+\\.[0-9]+\\.[0-9]+"
                    RESULT_VARIABLE COMPILER_VERSION_ERROR
                    OUTPUT_VARIABLE COMPILER_VERSION_OUTPUT
                    ERROR_VARIABLE COMPILER_VERSION_OUTPUT_ERROR
                    OUTPUT_STRIP_TRAILING_WHITESPACE
                    ERROR_STRIP_TRAILING_WHITESPACE)
        
            if (NOT ${COMPILER_VERSION_ERROR} EQUAL 0)
                message(FATAL_ERROR "Failed to query compiler version.\n\t\tError code or reason: ${COMPILER_VERSION_ERROR}.\n\t\tSTDERR: ${COMPILER_VERSION_OUTPUT_ERROR}")
            endif()
        
            if(${COMPILER_VERSION_OUTPUT} VERSION_LESS "13.2")
                message(FATAL_ERROR "Required GCC version 13.2 or later. Found ${COMPILER_VERSION_OUTPUT}.")
            endif()
        endif()

-nostdlib and -ffreestanding

For really space constrained firmware you might not want to link against the standard library and will want to stop GCC from assuming it can optimise certain functions based on the behaviour of the standard C library. For this you need the -nostdlib and -ffreestanding directives -- [Ref].

In freestanding mode, the only available standard header files are: <float.h>, <iso646.h>, <limits.h>, <stdarg.h>, <stdbool.h>, <stddef.h>, and <stdint.h> (C99 standard 4.6) -- [Ref].

# In addition this project will not link against the standard C library (-nostdlib and -ffreestanding)
#		-nostdlib      controls which libraries to link against
# 		-ffreestanding controls if you are compiling freestanding C (which is part of the C standard).
# 		 			   CAUTION: Even though freestanding is used it seems GCC still requires the freestanding
#                               environment provide memcpy, memmove, memset and memcmp
#                               [https://gcc.gnu.org/onlinedocs/gcc/Standards.html]
set(TARGET_COMMON_FLAGS ...)
list(APPEND TARGET_COMMON_FLAGS "-nostdlib" "-ffreestanding")
STRING(REPLACE ";" " " TARGET_COMMON_FLAGS "${TARGET_COMMON_FLAGS}")
        

Include CPPCheck In Your Projects

if (${USE_CPPCHECK})
    find_program(CMAKE_CXX_CPPCHECK NAMES cppcheck)
    if (CMAKE_CXX_CPPCHECK)
        MESSAGE(STATUS "Found CPPCheck: `${CMAKE_CXX_CPPCHECK}`")

        get_target_property(CPPCHECK_SOURCES ${PROJECT_NAME}${CMAKE_EXECUTABLE_SUFFIX} SOURCES)

        # Filter the source set if you need too.
        list(FILTER CPP_SOURCES INCLUDE REGEX ...)

        # CPP Check needs the include directories and the compile definitions that the
        # project uses. To get these use a generator expression:
        #     Generator expressions are evaluated during build system generation to produce
        #     information specific to each build configuration.
        set(CPPCHECK_INCS "$<TARGET_PROPERTY:${PROJECT_NAME}.elf,INCLUDE_DIRECTORIES>")
        set(CPPCHECK_DEFS "$<TARGET_PROPERTY:${PROJECT_NAME}.elf,COMPILE_DEFINITIONS>")

        # When using add_custom_command() or add_custom_target(), use the VERBATIM and 
        # COMMAND_EXPAND_LISTS options to obtain robust argument splitting and quoting.
        add_custom_target(
                ${PROJECT_NAME}_cppcheck
                ALL #< Adds to default target, remove to require distinct target to be made
                COMMAND ${CMAKE_CXX_CPPCHECK}
                ${CPPCHECK_SOURCES}
                "-I;$<JOIN:${CPPCHECK_INCS},;-I;>" #< Must be quoted so it is recognised as a generator expression
                "-D$<JOIN:${CPPCHECK_DEFS},;-D>"   #< Must be quoted so it is recognised as a generator expression
                --force
                --enable=all
                --suppress=missingIncludeSystem
                COMMAND_EXPAND_LISTS                     #< Lists in COMMAND arguments will be expanded, including those created with generator expressions
                VERBATIM                                 #< All arguments to the commands will be escaped properly for the build tool 
                USES_TERMINAL
            )
        
            # Or, apparently could use the following, which looks better:
            # "$<LIST:TRANSFORM,$<TARGET_PROPERTY:tgt,INCLUDE_DIRECTORIES>,PREPEND,-I>"
    else()
        MESSAGE(WARNING "Could not find CPPCHECK")
    endif()
endif()

Include DoxyGen In Your Projects

if (${USE_DOXYGEN})
    find_package(Doxygen)
    if (${DOXYGEN_FOUND})
        MESSAGE(STATUS "Doxygen found.")

        set(DOXYGEN_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/...choose a dir name...")
        set(DOXYGEN_IMAGE_PATH "${PROJECT_SOURCE_DIR}/...insert paths here...")
        set(DOXYGEN_RECURSIVE "YES")
        set(DOXYGEN_WARN_NO_PARAMDOC "YES")
        set(DOXYGEN_EXTRACT_ALL "YES")
        set(DOXY_INPUT "${PROJECT_SOURCE_DIR}/...list dirs here...")
        set(DOXYGEN_FILE_PATTERNS "*.c" "*.h" ...)
        set(DOXYGEN_EXCLUDE_PATTERNS ...anything you'd like to ignore...)
        doxygen_add_docs(
                ${PROJECT_NAME}_doxygen
                ALL #< Adds to default target, remove to require distinct target to be made
                ${DOXY_INPUT}
                COMMENT "Generate Doxygen content"
        )
    else ()
        MESSAGE(WARNING "Doxygen not found. Doxygen output will not be created.")
    endif ()
endif()
    

Misc

See search path used: if you want to see which directories CMake is search in your case just call

cmake -D CMAKE_FIND_DEBUG_MODE=ON

Dump out all variables and their values:

get_cmake_property(_variableNames VARIABLES)
foreach (_variableName ${_variableNames})
    message(STATUS "${_variableName}=${${_variableName}}")
endforeach()