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 theCMakeLists.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(MY_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 ";" " " MY_LINK_FLAGS "${MY_LINK_FLAGS}") set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "${MY_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 $<TARGET_FILE:${PROJECT_NAME}> ${HEX_FILE} COMMAND ${CMAKE_OBJCOPY} -Obinary $<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()