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.
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:
- Tell Conan to copy the relevant files from its
.conan/data
folder into a subfolder of thebuild
directory; and -
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