Writing Modern CMake Files
If you are a C++ developer, the word CMake shall not be a new word for you. The first time I know CMake was back in 2009 when I join the Hector team at TU Darmstadt to develop the elevation map from scratch. At that time, I learned the basic of CMake and then used it very minimal just to build my own component. Last year during the Self-Driving Car Engineer Nanodegree, I used and write CMake to build some C++ files. In November 2017, I encountered a challenge where I have to build software modules and libraries with CMake for multi-variant projects.
Supporting multi-variant projects mean that the platform libraries have to be compiled:
- in different hardware architectures (e.g. x86, x64, armv7),
- for different operating system (Linux, QNX, Integrity),
- for different customer projects with different variants (e.g. project X model A, B, C and project Y model H, I, J)
- for release and debug version
Note, CMake is a build system generator, not a build system.
My first effort
Directory structure
First of all, I have to make sure that the structure of the source code has the same layout. In my Linux OS, I define the root folder of the platform codes as platform
and create the subfolders like this:
/platform
library_a/
CMakeLists.txt
doc/
inc/
lib_a.h
src/
lib_a.cpp
library_b/
CMakeLists.txt
doc/
inc/
lib_b.h
interface.h
src/
lib_b.cpp
helper_b.cpp
app_a/
CMakeLists.txt
doc/
src/
main.cpp
Writing CMake
My first reference was the Mastering CMake book. Then, I read the official documentation and discussions in Stackoverflow. Reading the CMake introduction and the tutorials, I can create a CMake to build an application. The CMake content is written in the CMakeLists.txt.
An example to write a CMakeLists.txt (e.g. in AppA/) for an application written only in main.cpp without any dependency of library_a and library_b is:
cmake_minimum_required (VERSION 3.5)
project (AppA)
add_definitions(-std=c++11)
include_directories(/usr/local/include)
# Set a variable named sources
set(sources main.cpp)
# add the executable
add_executable (my_app ${sources})
I need include_directories(/usr/local/include)
due to the standard library that I used like #include <string>
in my main.cpp.
Next, I have such assumptions:
- the main.cpp depends on both libraries (library_a, library_b),
- library_a and library_b are built as shared libraries,
- the library_a needs a third party library, for example it is called thirdlib.
Then, based on the tutorials I wrote such a top CMakeLists.txt file:
project(AppA)
cmake_minimum_required (VERSION 3.5)
# Add directories which contains CMakeLists.txt
add_subdirectory (
${CMAKE_CURRENT_SOURCE_DIR}/../library_a/ libliba
${CMAKE_CURRENT_SOURCE_DIR}/../library_b/ liblibb)
This top-level CMakeLists.txt is placed under appa folder. The CMakeLists.txt under _library_a/ can be written as:
add_definitions(-std=c++11 -g)
set(LIBSOURCES src/lib_a.cpp)
include_directories(/usr/local/include)
# include the internal header files
include_directories(inc)
# Path of the third library called thirdlib
link_directories(/usr/local/lib)
# create the shared library
add_library(libliba SHARED ${LIBSOURCES})
# Link the thirdlib to libliba
target_link_libraries (libliba -lthirdlib)
So far the above CMake files can compile my code. However, after adding several libraries and delivering the library to the integration with another component in an integration server, I realized that the build process in different computers does not work. The reason is the include_directory
syntax that searches for a particular path in a system. Moreover, a third party library can be installed in different system path which causes an error in the link_directories(/usr/local/lib)
line.
The modern and effective approach
Daniel Pfeifer[1] and Mathieu Ropert[2] presentations give me a solution how to write CMake files effectively and efficiently to achieve a good modular design.
The main takeaway from their talks are:
- Use the latest CMake if possible or minimum the version 3.5.0.
- Keep the CMake code clean and clear.
- Avoid these commands:
add_compiler_options
,include_directories
,link_directories
,link_libraries
. -
Do not set
CMAKE_CXX_FLAGS
. Setting the C++ compiler via-std=c++14
inCMAKE_CXX_FLAGS
oradd_definitions(-std=c++11)
will brake in the future. Let CMake figures out itself. Instead, use thetarget_compile_features
and the CMake features. In order to compile the library with C++11 we can use this below example that uses the variable templates feature[3]. The disadvantage of this approach is that we cannot recognize directly whether a library need to be compiled using C++11 or C++14.target_compile_features(liba PRIVATE cxx_variable_templates)
- Don’t use
file(GLOB)
in projects. - Always explicitly declare properties
PUBLIC
,PRIVATE
, orINTERFACE
when usingtarget_*
. -
In order to build my own modular library I need to :
- add
add_subdirectory()
of the library in your top-level CMakeLists.txt - add
add_library()
in the CMakeLists.txt in the library folder. Optionally, use a namespace for your library, e.g.:add_library(myLibrary::liba ALIAS liba)
-
use
target_link_libraries()
to express direct dependencies in your application folder.target_link_libraries(my_app PUBLIC myLibrary::liba PRIVATE myLibrary::libb )
- add
- Keep internal properties
PRIVATE
. - Use the find module
find_package()
for third-party libraries that are not built by CMake. If you can find the*Config.cmake
file you don't have to create aFind*.cmake
. - Export your library’s interface, if you are a library author. One way is to use
BUILD_INTERFACE
andINSTALL_INTERFACE
generator expressions[4] as filters. See more in Daniel's slide[5], on page 26.
Rico wrote about the similar modern CMake guide[6]. His article helps me to understand both talks from Daniel and Mathieu.
Directory Layout
Finally, I can restructure my folder layout to become like this:
/platform
CMakeLists.txt
library_a/
build/
project-x/
CMakeLists.txt
project-y/
CMakeLists.txt
cmake/
FindLibthirdlib.cmake
doc/
inc/
lib_a.h
src/
lib_a.cpp
unittest/
test.cpp
library_b/
build/
project-x/
CMakeLists.txt
project-y/
CMakeLists.txt
doc/
inc/
lib_b.h
interface.h
src/
lib_b.cpp
helper_b.cpp
unittest/
test.cpp
app_a/
build/
project-x/
CMakeLists.txt
project-y/
CMakeLists.txt
doc/
src/
main.cpp
Assuming that the third-party library thirdlib doesn't support CMake (ThirdlibConfig.cmake doesn't exist in the system), I need to write my own FindLibthirdlib.cmake under cmake folder.
The top-level CMakeLists.txt
In order to build the above application app_a for the project-x variant, we can pass an argument -DSW_PROJECT=project-x
, for instance:
$mkdir /build && cd /build
/build$ cmake ../platform/ -DSW_PROJECT=project-x
Here, I can expand the above argument for the hardware variants by adding HW_ARCH=x64
.
An example of the top-level CMake:
cmake_minimum_required (VERSION 3.5)
# Differentiate between Windows platform and others
if (MSVC)
add_compile_options(/W3 /WX)
else()
add_compile_options(-W -Wall)
endif()
# Use GNUInstallDirs to install libraries into the correct path on all platforms.
include(GNUInstallDirs)
# Include third party library, e.g. thirdlib
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
find_package(Libthirdlib REQUIRED)
if(LIBTHIRDLIB_LIBRARIES)
message(STATUS "Top level cmake: libthirdlib found")
else()
message(STATUS "Top level cmake: libthirdlib not found")
endif()
add_library(libthirdlib INTERFACE IMPORTED)
set_property(TARGET libthirdlib PROPERTY
INTERFACE_INCLUDE_DIRECTORIES ${Libthirdlib_INCLUDE_DIR})
# dealing with the hardware variants
if(HW_ARCH)
#if(${HW_ARCH} STREQUAL "x32")
#...
#elseif(${HW_ARCH} STREQUAL "armv7")
#...
#else()
#...
#endif()
endif()
# Add the other CMake files to be built
add_subdirectory(library_a/build/${SW_PROJECT})
add_subdirectory(library_b/build/${SW_PROJECT})
add_subdirectory(app_a/build/${SW_PROJECT})
CMakeLists.txt in the library folder
From the CMake wiki, CMake projects or libraries that
are intended to be used by other projects should provide at a minimum a find_package()
command in config-mode to provide information about include-directories, libraries and their dependencies, required compile-flags or locations of executables.
cmake_minimum_required (VERSION 3.5)
# Define library. Only source files here!
project(libliba VERSION 1.0 LANGUAGES CXX)
add_library(liba SHARED ../src/library_a.cpp)
add_library(myLibrary::liba ALIAS liba)
# Compile using C++11
target_compile_features(liba
PRIVATE
cxx_variable_templates
)
# Define headers for this library. PUBLIC headers are used for
# compiling the library, and will be added to consumers' build paths.
target_include_directories(liba PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../inc>
$<INSTALL_INTERFACE:include>
PRIVATE ../src
)
# Link the third-party library that we defined in the top-level file
target_link_libraries(liba PRIVATE
thirdlib
)
# 'make install' to the correct locations (provided by GNUInstallDirs).
install(TARGETS liba EXPORT liba-config
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
INCLUDES DESTINATION inc # alternative use ${CMAKE_INSTALL_INCLUDEDIR} for ./include
)
# This makes the project importable from the install directory
# where the *config.cmake files are placed
install(EXPORT liba-config DESTINATION DESTINATION lib/cmake/)
# This makes the project importable from the build directory
export(TARGETS liba FILE liba-config.cmake)
By running make
, the file liba-config.cmake will be generated under the folder ./library_a. Both commands install(TARGETS ...)
and install(EXPORT...)
allow us to install the library and the *config.cmake file to the system folder. The command line make install
executes both CMake commands.
CMakeLists.txt in the application folder
# Define an executable called my_app
add_executable(my_app
../src/main.cpp
)
# Define the libraries this project depends upon
target_link_libraries(my_app
PUBLIC myLibrary::liba
PRIVATE myLibrary::libb
)
Conclusion
Applying the effective modern CMake approach allow me to design a modular C++ software. I can write clean and simple CMake files that can be distributed to other teams around the world. The software modules and the libraries will not be break even I distribute them in the different integration servers. One aspect that is still need an improvement is the use of CMAKE_CXX_FEATURES
, which is for me not so trivial how to map the features to the C++ compiler version.
##References