Skip to content

use_cmake

Naveau edited this page Feb 4, 2021 · 12 revisions

CMake?

CMake is a tool to compile c++ code (and other), build a complete project and install it in a good location. More information and tutorials can be found on the cmake website:

Entry point for cmake

Simple tutorial

Tutorial to create packages (this page is very much inspired by it)

An example for building projects with cmake

Here we take an example from the project kino-dyn-opt (the momentumopt package). In this project, we want to do the following:

  1. Compile some C++ code into a library (a library is just a file containing compiled code such that it can be reused by other programs)
  2. Compile some C++ code into a binary (i.e. a program that can be executed), which depends on the library
  3. Install the binary and library to a desired location
  4. Install the header files (.h or .hpp files that define the functions from the library) to the same location
  5. Copy some config files to the install location
  6. Install some Python scripts
  7. Compile and install Python bindings for the C++ code
  8. Define C++ and Python tests
  9. Create and install cmake files that will allow other packages to use the library
  10. Generate documentation either libraries (that can be used by other programs) or binaries (that can be executed by themselves) Note that libraries are just a package containing compiles functions and classes
  11. Compile tests and register them so they can be run using CTest and colcon or any other testing tool
  12. define what needs to be installed (or copied), and where
  13. create cmake files that will allow other packages to use the libraries and know how their dependencies
  14. install everything

Setting up the CMakeLists.txt

We create a file called CMakeLists.txt in the root of the project directory, it will contain all the instructions to do all of the above. The file starts with

#
# set up the project
#
cmake_minimum_required(VERSION 3.10.2)

project(momentumopt)

# specify the C++ 17 standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

The first line sets the minimum version of cmake required. The second line defines the project. It is also possible to define the project with a specific version (useful for more complex builds) by setting

project(momentumopt VERSION 1.0.0)

instead. The next lines set CMake variables to tell it that we use C++17 and require this standard (i.e. it will not be possible to compile with a lesser standard). It is often helpful to look up variables to see what they do, e.g. for CMAKE_CXX_STANDARD.

Then some compilation flags are set for all the targets we will compile in Release mode.

Now we define all the dependencies (i.e. the libraries we depend on)

#
# Dependencies
#
find_package(pybind11 CONFIG REQUIRED)
find_package(yaml_utils REQUIRED)
find_package(Eigen3 REQUIRED)

#local depends
find_package(mpi_cmake_modules REQUIRED)
find_package(solver REQUIRED)
find_package(solver_lqr REQUIRED)

Now we define a variable in which we will store a list of all the targets to compile (our library + our binary). We will NOT add to it our tests nor our python bindings, because tests are not be installed and Python bindings need to be installed elsewhere.

# variable to store list of targets
set(all_targets)

Building the targets (library + binary)

The library

First we list the source files that we need

# library source
set(momentumopt_SRC_FILES
    # files for contacts planning
    src/momentumopt/cntopt/ContactState.cpp
    src/momentumopt/cntopt/TerrainDescription.cpp
    src/momentumopt/cntopt/ContactPlanFromFile.cpp
    src/momentumopt/cntopt/ContactPlanInterface.cpp
    # files for dynamics planning
    src/momentumopt/dynopt/DynamicsState.cpp
    src/momentumopt/dynopt/DynamicsFeedback.cpp
    src/momentumopt/dynopt/DynamicsOptimizer.cpp
    # files for kinematics planning
    src/momentumopt/kinopt/KinematicsState.cpp
    src/momentumopt/kinopt/KinematicsInterface.cpp
    src/momentumopt/kinopt/KinematicsOptimizer.cpp
    # files for setting up environments
    src/momentumopt/setting/PlannerSetting.cpp
    # utilities
    src/momentumopt/utilities/OrientationUtils.cpp
    src/momentumopt/utilities/TrajectoryGenerator.cpp
)

Then we create our target (telling it which source files we need) and add it to our list

# Add Library.
add_library(momentumopt SHARED ${momentumopt_SRC_FILES})
list(APPEND all_targets momentumopt)

We tell cmake which libraries we need to compile this target

# Linking.
target_link_libraries(momentumopt solver::solver)
target_link_libraries(momentumopt solver_lqr::solver_lqr)
target_link_libraries(momentumopt yaml_utils::yaml_utils)
target_link_libraries(momentumopt Eigen3::Eigen)

Now we want to tell cmake that some defines in the C++ files need to be set to point to config files. When we build the library, it needs to point to the source folder and after the library has been installed, this needs to point to the place where the config files have been installed (yes this is fancy and probably not useful in most cases)

# Properties.
get_filename_component(CONFIG_PATH config ABSOLUTE)
target_compile_definitions(
  momentumopt
  PRIVATE CFG_SRC_PATH="${CONFIG_PATH}/"
  INTERFACE CFG_SRC_PATH="../share/${PROJECT_NAME}/config/")

Now we tell cmake where the includes are, first for the compilation and then after the lib has been installed

# Includes. Add the include dependencies
target_include_directories(
  momentumopt PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
                     $<INSTALL_INTERFACE:include>)

We are now done with the library.

Header only library

This paragraph is a little out of scope of the kino_dyn_opt but extremely relevant for this tutorial so here it is. If you have a header only library you can and must still create a library (cmake target) in order to ease the usage of your files by others.

As mentioned before we still call the add_library method (see for example yaml_utils):

# Headers only library.
add_library(${PROJECT_NAME} INTERFACE)

Notice here two facts:

  • There are no files included in this library
  • The keyword INTERFACE which indicate a header only library.

We still need to give this target the path to the includes:

# Add the include dependencies
target_include_directories(
  ${PROJECT_NAME}
  INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
            $<INSTALL_INTERFACE:include>)

And to affect to these include the list of their dependencies. As a normal target, if another package uses this library it will automatically depend on these dependencies too. This is the reason for creating such targets.

# Dependencies
target_link_libraries(${PROJECT_NAME} INTERFACE Eigen3::Eigen)
target_link_libraries(${PROJECT_NAME} INTERFACE ${YAML_CPP_LIBRARIES})

This was for creating a headers only library and export it.

The binary

This one if much simpler, as it only depends on the library we just defined

#
# Demos. (binaries)
#
add_executable(demo_momentumopt demos/demo_momentumopt.cpp)
target_link_libraries(demo_momentumopt momentumopt)
list(APPEND all_targets demo_momentumopt)

we define the target demo_momentumopt and its source file. We add a dependency on the target library we defined above and add this target to our list.

Install both targets

# command to install the library and binaries
install(
  TARGETS ${all_targets}
  EXPORT ${PROJECT_NAME}Targets
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  RUNTIME DESTINATION bin
  INCLUDES
  DESTINATION include
)

The library will be installed in installation_root_directory/lib and the binary in installation_root_directory/bin. Note that we do not need to define the installation directory in this file. We also tell cmake that we will create a cmake file called ${PROJECT_NAME}Targets that will contain instructions to find and use the library after it is installed.

Installing other files

We tell cmake to copy all the header files in include into installation_root_directory/include, to copy the folder config into installation_root_directory/share/${PROJECT_NAME} and to install the Python scripts (while removing their .py extension and making them executable) into installation_root_directory/bin. The function install_scripts is defined in our mpi_cmake_modules package in the cmake folder. This package contains useful macros and function to reduce code duplication.

# we also need to install the header files
install(DIRECTORY include/ DESTINATION include)

# the config folder
install(DIRECTORY config DESTINATION share/${PROJECT_NAME})

# Install the demo python files in bin
install_scripts(
  demos/demo_momentumopt_biped_from_python.py
  demos/demo_momentumopt_solo12_jump.py
  demos/demo_momentumopt_solo_jump_no_lqr.py
  demos/demo_momentumopt_solo_jump.py
  DESTINATION
  bin)

Compiling Python bindings (that use pybind11)

Now we can define the sources, create a module, tell cmake its dependencies with libraries (including the one we just compiled) and install it where python packages are installed

#
# Python bindings
#
set(pymomentumopt_SRC_FILES
    # files for contacts planning
    srcpy/momentumopt/cntopt/PyTerrain.cpp
    srcpy/momentumopt/cntopt/PyContacts.cpp
    # files for dynamics planning
    srcpy/momentumopt/dynopt/PyDynamics.cpp
    # files for kinematics planning
    srcpy/momentumopt/kinopt/PyKinematics.cpp
    # files for setting up environments
    srcpy/momentumopt/PyMomentumopt.cpp
    srcpy/momentumopt/setting/PyParams.cpp
    srcpy/momentumopt/setting/PySetting.cpp)
pybind11_add_module(pymomentum MODULE ${pymomentumopt_SRC_FILES})
target_link_libraries(pymomentum PRIVATE pybind11::module)
target_link_libraries(pymomentum PRIVATE momentumopt)

# install the bindings
get_python_install_dir(python_install_dir)
install(TARGETS pymomentum DESTINATION ${python_install_dir})

Note that get_python_install_dir(python_install_dir) is a macro defined in our mpi_cmake_modules package, it detects Python and figures out where python packages should be installed. In particular it feeds the input variable with the path, hence after calling it this way get_python_install_dir(banana_split) the variable banana_split will be defined and contain the path where the python module should be installed (with respect to our final install directory).

Install Python packages

In the python directory we have a package we'd like to install (while excluding certain files)

# install the python package too
install(
    DIRECTORY python/${PROJECT_NAME}
    DESTINATION "${PYTHON_INSTALL_DIR}"
    PATTERN "*.pyc" EXCLUDE
    PATTERN "__pycache__" EXCLUDE
  )

Compiling tests

Here we add tests using GoogleTest (for C++ tests) and pytest (for Python tests). We only do so if BUILD_TESTING is set to ON (enabled by default). We can disable testing when compiling, e.g. colcon build --cmake-args -DBUILD_TESTING=OFF

#
# Tests.
#
include(CTest)
if(BUILD_TESTING)
  # C++ unit-tests
  find_package(GTest REQUIRED)
  include(GoogleTest)
  add_executable(test_${PROJECT_NAME}
                    tests/gtest_main.cpp
                    tests/test_momentumopt.cpp)
  target_link_libraries(test_${PROJECT_NAME} momentumopt)
  target_link_libraries(test_${PROJECT_NAME} GTest::GTest)
  target_include_directories(
    test_${PROJECT_NAME} PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/tests/>
  )
  get_filename_component(TEST_PATH tests ABSOLUTE)
  set_target_properties(
    test_${PROJECT_NAME} PROPERTIES COMPILE_DEFINITIONS
                            TEST_PATH="${TEST_PATH}/yaml_config_files/")
  gtest_add_tests(TARGET test_${PROJECT_NAME})

  # Python tests
  add_test (NAME test_${PROJECT_NAME}_python
  COMMAND ${Python_EXECUTABLE} -m pytest ${CMAKE_CURRENT_SOURCE_DIR}/tests
)
endif()

Creating documentation

Here we use the macro defined in mpi_cmake_modules

#
# building documentation
#
add_documentation()

Creating and installing cmake files

We are almost done, we just need to create cmake files that will help other libraries find ours and use it. To do so we use a function defined in our mpi_cmake_modules.

note: This function installs the ${PROJECT_NAME}Target.cmake file. This file contain the list of the exported target created by the current package. Further if a VERSION was defined in the project, then the function will take care of this (e.g. it will be possible to test the version when calling find_package in another project).

#
# create the cmake package
#
generate_cmake_package()

IMPORTANT, in order for this function to work, we need to have a file called Config.cmake.in in the root directory. This file defines the dependencies and code needed for the libraries to be used afterwards. In our case it looks like

@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/momentumoptTargets.cmake")

include(CMakeFindDependencyMacro)

# we do not add the other dependencies because these are header files lib
find_dependency(yaml-cpp CONFIG REQUIRED)
find_dependency(Eigen3 REQUIRED)
find_dependency(solver REQUIRED)
find_dependency(solver_lqr REQUIRED)

check_required_components(momentumopt)

Note that it uses the file momentumoptTargets.cmake that we created when calling install on the targets above. We also specify which library is needed in addition to link this library after it is installed, also to forward the correct compile flags.

Compiling a dynamic-graph entity as an optionnal dependency.

This paragraph does not concern momentumopt but packages that uses the dynamic-graph. In order to use the dynamic graph we provide some cmake macros in mpi_cmake_modules that helps the build and installation. We provide here an optional dependency pattern also for sake of the tutorial. It is up to you to use dynamic-graph as an optional dependency or not in your packages.

# to be added in the header
find_package(mpi_cmake_modules QUIET)
find_package(dynamic-graph QUIET)
find_package(dynamic-graph-python QUIET)

set(build_dynamic_graph_plugins FALSE)
if(${mpi_cmake_modules_FOUND} AND
   ${dynamic-graph_FOUND} AND
   ${dynamic-graph-python_FOUND})
  set(build_dynamic_graph_plugins TRUE)
endif()

# Later in the CMakeLists.txt...

#
# Entities
#
if(${build_dynamic_graph_plugins})
  # plugin name
  set(plugin_name my_plugin_name)
  # Create the library
  add_library(${plugin_name} SHARED src/dynamic_graph/my_entity_1.cpp
                                    src/dynamic_graph/my_entity_1.cpp)
  # Add the include dependencies.
  target_include_directories(
    ${plugin_name} PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
                          $<INSTALL_INTERFACE:include>)
  # Link the dependencies.
  target_link_libraries(${plugin_name} dynamic-graph::dynamic-graph)
  target_link_libraries(${plugin_name}
                        dynamic-graph-python::dynamic-graph-python)
  target_link_libraries(${plugin_name} ${PROJECT_NAME}) # main library name is ${PROJECT_NAME}
  # Install the plugin's python bindings.
  install_dynamic_graph_plugin_python_bindings(${plugin_name})
  # Install the plugin.
  get_dynamic_graph_plugin_install_path(plugin_install_path)
  install(
    TARGETS ${plugin_name}
    EXPORT ${PROJECT_NAME}Targets
    LIBRARY DESTINATION ${plugin_install_path}
    ARCHIVE DESTINATION ${plugin_install_path}
    RUNTIME DESTINATION ${plugin_install_path}
    INCLUDES
    DESTINATION include)
endif()

Compiling and installing everything

with pure cmake

In the root directory

mkdir build
cd build
cmake ..
cmake --build

to install in the directory /my/arbitary/path/

cmake --install . --prefix "/my/arbitary/path/"

with colcon

In the workspace root directory (I do merge install to have a flat install structure)

colcon build --merge-install
Clone this wiki locally