CMake Best Practices
KeepTruckin’s Embedded Software Implementation of CMake
Often in C development, the build system is an afterthought, and the result is a messy, incoherent system that has been patched together as needed. These systems are typically difficult to change or append, due to the scale of the problem. Once a project gets off the ground and starts adding code to a repository, it can be extremely time-consuming and cost-prohibitive to change the infrastructure, because every developer now relies on it to do their daily tasks.
Enter the notion that when we create these projects, we should create them with the build system not only in mind, but as a central factor in how we design our repository infrastructure. When implemented correctly, CMake can be easy to use, promote better practices in system design, and make new infrastructure easy to bolt on.
The idea behind KeepTruckin’s embedded software implementation of CMake is to keep modules as small and independent as possible. To this end, we place modules into their own folders and only link other modules and libraries that are absolutely necessary. This promotes development in a way that is both platform-independent (portable) and easily extensible.
Creating a New Module
When creating a new module, the developer should first create the file structure. The following is our preferred file structure:
mymodule/
inc/
src/
test/
mock/
In the root directory of the module, we create a file called CMakeLists.txt
. This file defines the build, link, and test procedures for this module.
There are three types of modules: STATIC
, SHARED
, and INTERFACE
. STATIC
and SHARED
libraries are traditional in that they contain both source files that get compiled and header files that are either private or public; but INTERFACE
libraries simply provide a number of header files that define an interface.
The INTERFACE
type can be very useful when defining a generic class of modules in order to provide a layer of abstraction from the minutiae of a sensor interface. From experience, we prefer using statically compiled libraries, especially when building a system with circular dependencies, because that is the only way a project with this characteristic will compile.
Code Block 1: INTERFACE
Library
cmake_minimum_required(VERSION 3.7.2)project(lbb_core C)add_library(lbb_core INTERFACE)
Code Block 2: SHARED
library
cmake_minimum_required(VERSION 3.7.2)project(lbb_math_utils C)add_library(lbb_math_utils SHARED
src/lbb_math_utils.c
)
Including Directories
When developing a library or module, you may find it advantageous to expose some header files as an interface, while keeping some private for organization sake. Typically, putting public interface files in the inc
folder and private headers in src
is a good guideline.
There are other ways to do this, such as having multiple subdirectories in the inc
folder and defining include directories as PUBLIC
or PRIVATE
, but I find that this quickly gets messy.
To set up include directories for the module, use target_include_directories
, as opposed to any other target-generic version of this function. Explicitly adding include directories for a target reduces confusion and allows other modules that link the library to automatically have access to the PUBLIC
include directories given to this function. As an aside, interface modules can only include directories as INTERFACE
permissions level.
Code Block 3: INTERFACE
Library Include Directories
target_include_directories(lbb_core
INTERFACE
inc/
)
Code Block 4: SHARED
Library Include Directories
target_include_directories(lbb_math_utils
PUBLIC
inc/
)
Linking Libraries
Linking libraries is much like including directories in CMake, and carries many of the same concepts. Using target_link_libraries
, a module can include any library’s public interface that is included in the same top level target (usually an executable).
PRIVATE or PUBLIC?
As a rule, if a library uses another module in its public interface, it should link that module as PUBLIC; otherwise, it should link it as PRIVATE. This ensures that all compilation dependencies are met, while providing interfaces only on a “need-to-know” basis.
Code Block 3: Linking libraries
target_link_libraries(lbb_math_utils
PUBLIC
lbb_units PRIVATE
lbb_core
lbb_sort
)
Installing Targets
Sometimes a developer wants to create a library and use it elsewhere, outside of its project structure. If this is the case, it is best to “install” the library to a specified deployment location. For each CMake build, an environment variable is defined to tell the scripts where this deployment directory is (currently this variable is called DESTDIR
; this may change in the future). This can be hard-coded in some systems, but I’ve found it useful to pass in the destination from an environment variable that can be used elsewhere in the build system.
Code Block 4: Installing Targets
install(
TARGETS
lbb_math_utils
DESTINATION
“/$ENV{DESTDIR}”
)
Often when developing a module or library, the developer wants to link to it from outside the repository. The requirements to do this include the above installation, as well as an “installation” of the public header files. The following is an example of how to do so using LBB_COMMON_INCLUDES_PATH
, which is defined as an environment variable.
Code Block 5: Installing Targets for External Use
install(
DIRECTORY
inc/
DESTINATION
“/$ENV{LBB_COMMON_INCLUDES_PATH}”
)
Creating Tests
Most of the time, when creating a new module, a developer wants or is required to add unit tests around their module. At KeepTruckin, we define an environment variable called TEST
and set it to ‘1’ when a test is being executed in order to conditionally compile these test binaries. The developer must create an executable using the add_executable
macro with a new target name (usually the module name with ‘_test
’ appended for clarity). In order to compile some tests, libraries must be linked, much like for the module itself, and then add_test
is used to notify CMake that this binary is for unit testing.
Code Block 6: Test Example
if(“$ENV{TEST}” EQUAL “1”)
# Add a unit test executable
add_executable(lbb_math_utils_test
test/lbb_math_utils_test.c
)
target_link_libraries(lbb_math_utils_test
PRIVATE
lbb_math_utils
lbb_core
unity
m
)
add_test(LBBMathUtilsTest lbb_math_utils_test)
endif()
Complete Example
Code Block 7: Full SHARED
Library Example
cmake_minimum_required(VERSION 3.7.2)project(lbb_math_utils C)add_library(lbb_math_utils SHARED
src/lbb_math_utils.c
)target_include_directories(lbb_math_utils
PUBLIC
inc/
)target_link_libraries(lbb_math_utils
PRIVATE
lbb_core
lbb_sort
)# Install the target
install(
TARGETS
lbb_math_utils
DESTINATION
“/$ENV{DESTDIR}”
)if(“$ENV{TEST}” EQUAL “1”)
# Add a unit test executable
add_executable(lbb_math_utils_test
test/lbb_math_utils_test.c
)
target_link_libraries(lbb_math_utils_test
PRIVATE
lbb_math_utils
lbb_core
unity
m
)
add_test(LBBMathUtilsTest lbb_math_utils_test)
endif()
Code Block 8: Full INTERFACE
Library Example
cmake_minimum_required(VERSION 3.7.2)project(lbb_core C)add_library(lbb_core INTERFACE)target_include_directories(lbb_core
INTERFACE
inc/
)# Install the target
install(
TARGETS
lbb_core
DESTINATION
“/$ENV{DESTDIR}”
)
Additional Considerations
Linking to External Libraries
Assuming an external library has followed the steps for installing its artifacts, the consumer of the library knows where the artifacts exist. When linking from an external library, the developer must tell CMake where the artifacts exist, using find_library
for the library files (*.a
, *.so
) and include_directories
for the header files.
Code Block 9: Retrieving External Library References
# find external libraries and cache their whereabouts
find_library(LbbJournal lbb_journal PATHS ENV LBB_COMMON_DEPLOY_PATH)
find_library(UbloxGpsIntf ublox_gps_intf PATHS ENV LBB_COMMON_DEPLOY_PATH)# add include paths for external libraries
include_directories(“$ENV{LBB_COMMON_INCLUDES_PATH}”)
This code stores the location of the libraries in LbbJournal
and UbloxGpsIntf
, which are cached variables that can be used by CMake during the build process, and provide CMake with the include directories for these libraries’ header files. The following is an example of how to use these cached variables to link the external libraries into a module.
Code Block 10: Linking External Library
target_link_libraries(gnss_app
PRIVATE
“${LbbJournal}”
“${UbloxGpsIntf}”
)
Creating an Executable
The add_executable
function is used to define an executable target. It works much like add_library
.
Code Block 11: Creating an Executable
add_executable(${EXECUTABLE_NAME}
startup/startup_stm32f413xx.s
)
Intermediate CMakeLists.txt
Files
To define the structure of the build repository intermediate files, use add_subdirectory
to point to more CMakeLists.txt
files.
Code Block 12: Intermediate Subdirectory Inclusion
add_subdirectory(diag_evt_utils)
add_subdirectory(lbb_assert)
add_subdirectory(lbb_core)
add_subdirectory(lbb_data)
add_subdirectory(lbb_env)
add_subdirectory(lbb_errcode)
add_subdirectory(lbb_journal)
add_subdirectory(lbb_math_utils)
add_subdirectory(lbb_sort)
add_subdirectory(lbb_time)
add_subdirectory(lbb_units)
ToolChain Files
To provide cross-compilation support, CMake supports using toolchain files that are passed in on the command line at configuration time. My preference is to define the compiler, global compile options (used for compile, link, and assembler), extra compiler flags, extra linker flags (such as using a linker file), and extra assembler flags in this file.
Code Block 13: Toolchain File Example
set(CMAKE_SYSTEM_NAME FreeRTOS)
set(CMAKE_SYSTEM_VERSION 9.0.0)
set(CMAKE_SYSTEM_PROCESSOR arm)set(tools $ENV{LBB_SDK}/compiler/toolchain)
set(CMAKE_C_COMPILER “$ENV{CC}gcc”)# GLOBAL OPTIONS
set(GLOBAL_OPTIONS
-mthumb
-mcpu=cortex-m4
-mfloat-abi=softfp
-mfpu=fpv4-sp-d16
)
add_compile_options(“${GLOBAL_OPTIONS}”)# COMPILER OPTIONS
set(COMPILE_OPTS
-std=gnu11
-DUSE_HAL_DRIVER
-DSTM32F413xx
-DPRI_SYSTEM_INFO
-DPRI_SYSTEM_ERR
-O0
-ffunction-sections
-fdata-sections
-g
-fstack-usage
-Wall
)
string(REPLACE “;” “ “ COMPILE_OPTS_STRING “${COMPILE_OPTS}”)
set(CMAKE_C_FLAGS “${COMPILE_OPTS_STRING}” CACHE STRING “” FORCE)# LINKER OPTIONS
set(LINKER_SCRIPT
“STM32F413ZH_FLASH.ld”
)
set(LINKER_OPTS
“-specs=nosys.specs -static -Wl,-Map=${PROJECT_NAME}.map -Wl, — gc-sections -Wl, — defsym=malloc_getpagesize_P=0x80 -Wl, — start-group -lc -lm -Wl, — end-group”
)
set(CMAKE_EXE_LINKER_FLAGS “${LINKER_OPTS}” CACHE STRING “” FORCE)# ASSEMBLER OPTIONS — appended to COMPILE_OPTS
set(ASSEMBLER_OPTS
-x assembler-with-cpp
-g
-Wa, — no-warn
)
string(REPLACE “;” “ “ ASSEMBLER_OPTS_STRING “${ASSEMBLER_OPTS}”)
set(CMAKE_ASM_FLAGS “${ASSEMBLER_OPTS_STRING}” CACHE STRING “” FORCE)