A Relevant Example with CMake
Executive Summary
CMake is the gold-standard for cross-platform development, yet its documentation is pretty awful. The API docs available on the developer’s site are confusing and lacking in concrete examples. Surprisingly, there are not a lot of books on the topic either. I find this last bit particularly strange considering how widely used CMake is. Doing all but the simplest add_executable
-type projects can be very difficult. In this post, I attempt to provide a concrete example of how to use CMake to build a project with interdependencies, export/import the packages that the project built, and build unit tests. It is my hope that, with this example, many concepts about CMake will become more clear (this was certainly the case for me).
Why this post?
A lot of interesting robotics packages I have found over the last several months have used the catkin build system, which I have had only limited exposure to. For those of you not “in the know”, catkin is a build system that achieves goals of the original developers of the Robot Operating System (ROS), Willow Garage. Personally, my professional projects don’t use ROS and, thus, it becomes difficult for me to try out certain new open-source software at work because we don’t now, or ever intend to, use ROS. To bridge this gap, I wanted to find a candidate package built using ROS/catkin and convert it to native CMake, which is a universal build tool for multi-platform development. If I were to be successful in this task, conversion of other packages could be accomplished with relative ease, which would therefore enable quicker prototyping of candidate algorithms/approaches to my professional tasks.
I’ll preface the rest of this post by saying the following: What little exposure I do have to catkin is not positive. Specifically, I have the following axes-to-grind:
- In-source builds. When building a catkin project, you have to put the project into your catkin workspace under a
src
folder. As a developer, why in the hell would I want anything other than actual source files undersrc/
? - ROS as a dependency to build my project?. To build your shiny new catkin package, you need to source a ROS environment file, which means you need ROS. Indigo, a previous ROS distribution, takes upwards of 375MB of disk space. That’s a lot of space for some stuff to help me build executables.
- make and make_isolated… Seriously?. I have had troubles in the past with the build process failing with some cryptic message when running
catkin_make
only to have the build process succeed withcatkin_make_isolated
and then have various problems downstream.
Am I an expert in build systems? Definitely not. As a developer, I have mostly inherited build environments that have had the infrastructure already fully implemented, or nearly so. It may therefore be true that my understanding of what catkin is, and is not, is fundamentally flawed. Be that as it may, my understanding of what is required of my build systems is not flawed. I would like my build systems to observe the following rules-of-thumb:
- Minimal software. Ideally, I would like to just use cmake and make.
- Out-of-source builds. This means that a pristine source tree is maintained; build products are segregated.
- Simple
install
anduninstall
procedures. - “superbuild” and “build individual package” capabilities. Projects with multiple submodules and interdependencies should have build modes for building the whole project as well as building each submodule separately.
- Export packages. It should be easy to export and, more importantly, use built packages in other projects later on down-the-line.
- Export ALL package dependencies. This is related to the above bullet, but goes a step further. Let’s say I have two projects,
A
andB
, and thatB
depends onA
. If I have a third projectC
that depends onB
alone, I should not have to import bothA
andB
in order to build and runC
’s targets. In other words,B
’s export method needs to correctly handle all of its dependencies.
I will address, in order, each of these bullet points by starting with this GitHub repo. I chose this repo because it builds projects with interdependencies, is relatively small (in terms of number of source files), has unit tests, and used a catkin build system. I say “used” because, since I created my fork, the repo has been updated to support a native CMake build. I should mention here that my approach is my own, not that of the original repo’s author. For my approach, please see my fork. I’ll begin with a brief discussion of the original repo. First, the structure:
./
ifopt/
ifopt_core/
ifopt_ipopt/
ifopt_snopt/
...
ifopt
contains the CMakeLists.txt
file which sets up a catkin_metapackage()
, which is a package of packages. Each of the remaining three folders each contain its own package: src/
, include/
, test
, and a CMakeLists.txt
to build it (again, each using catkin
). ifopt_ipopt
and ifopt_snopt
each depend on the build products of ifopt_core
; and each submodule has unit tests built with GoogleTest to verify correct functionality. This small repo has a lot going on, in terms of build concerns. This is why I chose it as a starting point. In each subsequent section, I will present code snippets that demonstrate the application of my high-level requirements/desires in my final design.
Minimal Software
The original repo uses catkin, which is undesirable based upon my first bullet point above. The first objective before doing any other work was to find a way to remove all catkin dependencies, which meant eliminating ifopt
from the build stage, entirely. To do this, I added a root-level CMakeLists.txt
. The metapackage-business is replaced by CMake’s add_subdirectory(...)
macro:
# the first argument is the package root directory relative to $CMAKE_CURRENT_SOURCE_DIR
# the second argument is build location relative to $CMAKE_CURRENT_BINARY_DIR (the directory you ran `cmake` from)
add_subdirectory(ifopt_core ifopt_core)
add_subdirectory(ifopt_ipopt ifopt_ipopt)
add_subdirectory(ifopt_snopt ifopt_snopt)
This basically just tells CMake to go down into these directories and execute instructions from their CMakeLists.txt
files.
After doing this, all catkin dependencies are gone. Woohoo!
Out-of-Source Builds
This is what makes CMake so great: I can run cmake path_to_CMakeLists_file
from anywhere I want and this will set the current directory up as a build directory. If you are deadset on having your CMake files co-mingled with source files, I believe you can do this, but it won’t be pretty! With catkin, I need to have a workspace set up, and I need to have proper environment variables sourced, and … At the end of this process, my build can only be executed in one spot, under catkin_ws/src
. Ugh… No thanks! By setting up the CMakeLists.txt
file in the project root directory, I have done away with catkin’s in-source build protocol in ( nearly ) one stroke.
Simple Install/Uninstall
CMake is great at this, and why wouldn’t it be? It just builds on make
in this regard. I have created a simple uninstall method in the root CMakeLists.txt
file. This uninstalls all build products from the superbuild at once. In contrast, each submodule can be installed individually, but the uninstall must be done in bulk. This is because I setup the uninstall target as custom, and more than one custom target in a project cannot have the same name. I suppose that I could have created a custom uninstall target within each submodule, e.g. uninstall_ifopt_core
etc…, but this wasn’t a major concern for me. While I wanted to be able to build each submodule independently of one another, for installation my desire was different. What I really wanted when running the install/uninstall step, was to install/uninstall all project targets at once.
Building Individual Packages vs. All
Because of the way the add_subdirectories
macro works, CMake enables building all submodules together, provided you have correctly set up the interdependencies, or you can navigate to individual submodules and build them separately. CMake has the capability, but it takes some doing to set up.
Export Packages
For this part, I’ll admit, the original repo wins. Catkin handles all of the heavy-lifting behind-the-scenes. The other requirements trump this one, however, so I needed to find a way to make CMake do this for me.
This turned out to be surprisingly difficult given the lack of useful documentation. I found a few blog posts that were useful, specifically this one, however none seemed to have concrete example code to play around with. This repo was very useful, and I was able to figure out a lot of what I needed to do from it, but there were pain points; the biggest of which was that the project was header-only, meaning that it did not export any libraries. Great for them and people wanting to work with their repo, but not great for me. I ended up building my solution from many Google searches and trial-and-error. The solution I arrived at came from the following considerations:
- The CMake macro
find_package()
should be used for finding both internal and external dependencies - Packages need to export both their targets and their dependencies. Not doing so could create compile- and/or runtime errors.
From ifopt_core/CMakeLists.txt
:
set_target_properties(ifopt_core PROPERTIES
LINK_INTERFACE_LIBRARIES ${PROJECT_BINARY_DIR}/libifopt_core.a
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/include)
############
## EXPORT ##
############
# for local build
export(PACKAGE ifopt_core)
get_property(ifopt_core_INCLUDE_DIRS DIRECTORY
${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
get_property(ifopt_core_LIBRARIES TARGET ifopt_core PROPERTY LINK_INTERFACE_LIBRARIES)
configure_file(ifopt_coreConfig.cmake.in
"${PROJECT_BINARY_DIR}/ifopt_coreConfig.cmake" @ONLY)
# for external build
set(ifopt_core_INCLUDE_DIRS ${CMAKE_INSTALL_PREFIX}/include ${EIGEN3_INCLUDE_DIR})
set(ifopt_core_LIBRARIES ${CMAKE_INSTALL_PREFIX}/lib/libifopt_core.a)
configure_file(ifopt_coreConfig.cmake.in
"${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/ifopt_coreConfig.cmake" @ONLY)
With the above snippet, I have achieved:
- The correct setup of the intermediate build product as an interface:
ifopt_core
, upon whichifopt_ipopt
andifopt_snopt
depend, is built as an interface library, meaning that CMake will set it up so that consumers of this target won’t be built until after it has been built. - The creation of both a local and a global export method setup. This ensures that both local build products and those from separate projects can set the appropriate CMake environment variables, e.g.
ifopt_core_INCLUDE_DIRS
andifopt_core_LIBRARIES
.
Take note of the ifopt_*Config.cmake
files, as these enable find_package
to correctly set the environment variables.
Export All Package Dependencies
CMake handles this with aplomb. To make sure that I can call find_package()
on something which is not self-contained, I can simply do the following (from ./ifopt_core/CMakeLists.txt
):
set(ifopt_ipopt_INCLUDE_DIRS ${CMAKE_INSTALL_PREFIX}/include include ${ifopt_core_INCLUDE_DIRS} ${IPOPT_INCLUDE_DIRS})
set(ifopt_ipopt_LIBRARIES ${CMAKE_INSTALL_PREFIX}/lib/libifopt_ipopt.a ${ifopt_core_LIBRARIES} ${IPOPT_LIBRARIES})
Basically, just make sure you add all dependencies to the environment variables you wish to pull in with a find_package()
call and it should just work.
Testing
Now, on to the most important part: Testing. One of the main reasons I chose the original repo as a starting point was because it had several unit tests for each submodule that could be used to verify the build process. To follow along, follow the steps here.
Basically, you will need to clone the GoogleTest repo (just follow the instructions in the README) and follow the remaining instructions to build. Some other things to consider:
- I don’t use SNOPT, I use IPOPT for optimization problems. You should take the SNOPT implementation with a grain-of-salt. It should work, but I did not test.
- The IPOPT install I used came from a debian package installed via
sudo apt install...
. Specific definitions for building with this library are included where appropriate (see here).
From the project root, I was able to verify that the process works by executing the following:
$ mkdir build ; cd build ; cmake .. -DBUILD_TEST=True
$ make
$ make test
The output of the last step indicated that both unit tests ran successfully:
Test project /home/joe/github/ifopt/build
Start 1: ifopt_core-test
1/2 Test #1: ifopt_core-test .................. Passed 0.00 sec
Start 2: ifopt_ipopt-test
2/2 Test #2: ifopt_ipopt-test ................. Passed 0.01 sec
100% tests passed, 0 tests failed out of 2
Total Test time (real) = 0.02 sec
This verifies the local export method is working as expected, since ifopt_ipopt
used find_package(ifopt_core REQUIRED)
in its CMakeLists.txt
file. Now, it remains to verify the external method works.
Before doing the verification, the targets of the local build need to be installed where CMake can find them:
$ sudo make install
To do this, I created a dummy project with the unit tests, but out of the original build tree and with a new CakeLists.txt
file:
cmake_minimum_required(VERSION 2.8.3)
project(ifopt_test)
add_compile_options(-std=c++11)
find_package(ifopt_core REQUIRED)
find_package(ifopt_ipopt REQUIRED)
add_subdirectory(third_party/gtest third_party/gtest)
include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR}
${ifopt_core_INCLUDE_DIRS})
add_executable(ifopt_core-test
src/gtest_main.cc
src/composite_test.cc
src/problem_test.cc
)
target_link_libraries(ifopt_core-test ${ifopt_core_LIBRARIES} gtest gtest_main pthread)
include_directories(${ifopt_ipopt_INCLUDE_DIRS})
add_executable(ifopt_ipopt-test
src/ex_test_ipopt.cc
)
target_compile_definitions(ifopt_ipopt-test PRIVATE HAVE_CSTDDEF)
target_link_libraries(ifopt_ipopt-test ${ifopt_ipopt_LIBRARIES} gtest gtest_main pthread)
The directory structure of this test project should be clear, at this point:
./
src/
third_party/
gtest/
...
CMakeLists.txt (the one written above)
Inside of the src
directory are copies of the unit test source files from the original repo. Running this shows that the executables run correctly, meaning that the global export method works as well.
Before moving on, you should note that ifopt_ipopt-test
, which depends upon ifopt_core
, ifopt_ipopt
and IPOPT
, now only requires linkage against ${ifopt_ipopt_LIBRARIES}
, meaning that dependencies have been handled with the previously mentioned best practices in mind.
Parting Thoughts
In this post, I discussed, at a high-level, build systems and some things to consider when employing them to actual projects. I felt that there was a lack of useful and relevant examples online for building complicated CMake projects, so I created one.
I make no claims that the approach I took and discussed above is the only one, or even that it is the best approach. I am only trying to demonstrate some features and capabilities I discovered while working with a set of simple guidelines in mind (outlined by the bullet points above). I have verified that the work presented is correct in the sense that I got meaningful outputs from the test cases considered, which were hardly exhaustive. If you believe something was done in error, feel free to leave a comment.
I hope that this post helps you in your future work with CMake! It is highly useful for build management and, perhaps most importantly, it is free :) Thanks for reading!