Converting a C++ project to CMake & Conan

This post describes how to fully automate the build/install of a C++ Project, with several dependencies, using CMake and Conan, a package manager.

A couple of years back, I needed a simple HTTP client to run some integration tests against a Gossip Server I was developing; it turns out that, something we take for granted in Java, is a rather complicated matter to obtain for a C++ project: I eventually found SimpleHttpRequest and decided to give it a spin.

However, if you look at the project how it was back then it was still a pretty cumbersome build process, including, among other things, cloning three sub-modules, and then building and installing them manually.

Enter Conan

Conan is an open-source project that simplifies building C++ libraries that your own project depends upon, across binary platforms and without having to really to dig into the intricacies of how each library build is designed.

SimpleHttpRequest had three dependencies which it imported directly into the repository (http-parser, libuv and opensssl), the conanfile.txt required to download and build them would be something like this:

[requires]
openssl/1.1.1d
libuv/1.31.0@bincrafters/stable
http_parser/2.9.2

To find packages, one can either use the conan search command, or browse the repository hosted by JFrog; Conan is a Python module, so it can be installed with pip:

sudo -H pip install conan

Finally, you can download and build the necessary libraries with:

conan install ${BASEDIR} -if=${BUILD_DIR} -pr=default --build=missing

(see the build.h script for more details).

Conan will auto-detect your C++ compiler (I strongly recommend the use of clang over gcc) and other system properties, but you can override them by modifying the default profile, kept in ~/.conan/profiles/default:

└─( cat .conan/profiles/default
[settings]
os=Linux
os_build=Linux
arch=x86_64
arch_build=x86_64
compiler=clang
compiler.version=9
compiler.libcxx=libstdc++11
build_type=Debug

All going well (and there is a lot that can go wrong: we’re building C++ libraries, after all) the packages will be downloaded, built and installed in various subfolders of .conan/data.

This is all good and well, but how do we make them available to our code, headers and libraries?

This is where the magic of CMake comes into play: by having this in your conanfile.txt:

[generators]
cmake

will tell Conan to generate a few files, which will be in your project’s build folder, in particular conanbuildinfo.cmake which will be included in your CMakeLists.txt:

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)

CMake to rule them All

CMake is an open-source, cross-platform family of tools designed to build, test and package software.

CMake.org

If you, like me, hate to enter long sequence of manual commands and simply cannot be bothered to remember compile flags and configuration options, CMake is the tool for you.

It automates a lot of functionality and makes building an entire project as simple as running:

cmake ..
cmake --build . --target all -- -j 6

The downside is that one must learn the CMakeLists.txt file syntax and CMake’s commands, which are not the most intuitive, or easy to remember: luckily, there is ample documentation, and tons of online examples.

The major upside is that now you have fully automated and self-documented your project’s build process, it is a one-off exercise, and you can build on top of it as requirements change.

The full CMakeLists.txt is here, in the following, I will focus on a few chosen areas.

Conan integration

As mentioned, once dependencies are built, the header files and compiled libraries need to be made available to your project’s build toolchain: this is done via a couple of simple commands:

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()

After this, all the header files from the libraries defined in conanfile are available in the include_directories() and the libraries (lib*.so and .a files) are available to use in target_link_libraries():

set(LIBS
        http_parser
        pthread
        uv
)

// ...

add_executable(example_with_ssl
    ${SRC}/example.cpp
)
target_link_libraries(example_with_ssl
    ${LIBS}
    ssl
)

Installing libraries

Once built, it is usually convenient to “install” the headers and libraries somewhere from where they can be reused in other projects.

Here, we also need to get access to the library dependencies, as they are all built as shared libraries (.so on Linux systems, and *.dyld on MacOS ones).

This is done in two steps:

  1. Tell Conan to copy the relevant files from its .conan/data folder into a subfolder of the build directory; and

  2. Tell CMake where to install the finished artifacts.

In conanfile.txt we add:

[imports]
lib, *.dylib* -> ./lib
lib, *.so* -> ./lib
include, * -> ./include

and in CMakeLists.txt:

    # Install Libraries and Headers.
    install(DIRECTORY ${CMAKE_BINARY_DIR}/include/ DESTINATION
            ${INSTALL_DIR}/include
            FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp")
    install(DIRECTORY ${CMAKE_BINARY_DIR}/lib/ DESTINATION
            ${INSTALL_DIR}/lib
            FILES_MATCHING PATTERN "*.so*" PATTERN "*.dylib*")

We further allow the user to define where the “install directory” should be, by using either $INSTALL_DIR environment variable, or -DINSTALL_DIR=/some/path when invoking cmake:

if(NOT DEFINED INSTALL_DIR)
    if(DEFINED ENV{INSTALL_DIR})
        set(INSTALL_DIR $ENV{INSTALL_DIR})
    endif()
endif()

(note the difference between ENV{INSTALL_DIR} — the variable itself — and $ENV{INSTALL_DIR}, its contents).

Putting it all together

The final step in automating the entire build process is to pull the various strands together into a build.sh script:

BASEDIR="$(abspath $(dirname $0))"
BUILD_DIR="${BASEDIR}/build"

mkdir -p ${BUILD_DIR}
cd ${BUILD_DIR}

conan install ${BASEDIR} -if=${BUILD_DIR} -pr=default --build=missing

if [[ -d ${COMMON_UTILS_DIR} ]]; then
  UTILS="-DCOMMON_UTILS_DIR=${COMMON_UTILS_DIR}"
fi

cmake -DCMAKE_CXX_COMPILER=$(which clang++) ${UTILS} ..
cmake --build . --target all -- -j 6

so the project can be built and installed using:

./build.sh && cd build && make install

Learning more about using Conan / CMake

If you are interested in a more complex example of how to use Conan and CMake to build a C++ project, including using Google Protocol Buffers and running unit tests with Google GTest, please see my Gossip Protocol for Fault Detectors project.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s