License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright : Jeremy Fix, CentraleSupelec
Last modified : April 26, 2024 02:04
Link to the source : index.md

Table of contents

CMake is a production engine : wait….a what ?!

You already know how to compile with g++; if you were to compile a shared library, you would do something like

mylogin@mymachine:~$ g++ -shared -fPIC -std=c++17 -o libmylib.so mylib-obj1.cpp mylib-obj2.cpp $(pkg-config --libs --cflags mydependency1)

And if you were to compile some binaries dependending on that shared library, you would do something like

mylogin@mymachine:~$ g++ -o sample sample.cpp -L. -lmylib
mylogin@mymachine:~$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
mylogin@mymachine:~$ ./sample

And that’s great but clearly cumbersome if you have a large project and needs to define manually all these rules. In addition, you need to schedule the rules correctly: mylib needs to be compiled before the examples (especially if the library is static and not dynamically loaded). Finally, which part of your software needs to be recompiled as soon as, say, mylib-obj2.cpp is modified ? There exists various tools that allow you to automatize that process of defining the compilation lines and scheduling these compilation steps. These tools are called build automation utilities and popular ones are GNU Makefile, scons, cmake, distutils, gradle, ant, … In this tutorial, we focus on CMake, that’s a choice.

As a side note, there are also build automation servers which are in charge of executing some tasks automatically under some conditions. To give you an example, the webpage you are reading is compiled automatically by a continuous integration gitlab server as soon as its markdown source is pushed on the server. Automation tools are fantastic.

Compiling with CMake

In this tutorial, we will be analyzing step by step how to package a project which :

That’s our menu, ready to go ? Fasten your seat belt…Oh wait, did I tell that it is going to be quite tough ? It is going to be quite tough because your are learning to program, the compilation questions are probably not that familiar to you, the questions about handling dependencies might just be an unknown territory but keep on working and ask questions to your teammates and teachers. And see here, some well known projects do use CMake !

General principles

The pipeline with CMake is depicted below. The cmake command line tool is processing your sources according to configuration files named CMakeLists.txt and produces Makefiles and possibly generates or configures other files. From these, the make command line can be issued and triggers all the necessary builds. All the smart things are defined in the CMakeLists.txt files (yes, there can be multiple such files).

To install CMake, simply call :

mylogin@mymachine:~$ sudo apt install -y cmake

You can also access the documentation online.

Last, before staring, be sure that you have the gtkmm-3.0 developpement package available on your system, since incomming compiling will need that. You can test by

mylogin@mymachine:~$ ($(pkg-config --exists gtkmm-3.0) && echo "gtkmm-3.0 is found.") || echo "gtkmm-3.0 is not found."

and, if necessary, install by

mylogin@mymachine:~$ sudo apt install -y libgtkmm-3.0-dev

You will also need opencv to be installed.

mylogin@mymachine:~$ ($(pkg-config --exists opencv) && echo "OpenCV is found.") || echo "OpenCV is not found."
mylogin@mymachine:~$ sudo apt install -y libopencv-dev

A fully functional example

Compiling, installing and running

We will be studying the full example quantize.tar.gz. That is a fully functional example that you can use as a cooking recipe. The idea of that tutorial is that, when you need to define CMakeLists for your future projects, you come back later reading the section answering your needs. In the next sections, we will detail every single component of the project. Before doing so, let us take the project and compile it to see the end result.

Take the archive and unpack it in a suitable directory.

mylogin@mymachine:~$ tar -zxvf quantize.tar.gz
mylogin@mymachine:~$ cd quantize

cmake projects can be compiled out of source. That means we make a build directory, clearly different from the sources, in which we build our project. By convention we make a build directory. Building out of source is not mandatory, that’s just a convention but that keeps your source directory clean.

mylogin@mymachine:~/quantize/$ mkdir build
mylogin@mymachine:~/quantize/$ cd build

There, we now invoke the cmake command line (CLI) tool that will generate the files for building our project.

mylogin@mymachine:~/quantize/build/$ cmake .. -DCMAKE_INSTALL_PREFIX=~/.local/ 

And from there, we finally build and install the project (the -j activates parallel compilation) :

mylogin@mymachine:~/quantize/build/$ make -j 
mylogin@mymachine:~/quantize/build/$ make install 

This will build the project then install the project into the ~/.local/ subdirectories. It is a standard practice, when you do not have root access or want to perform a user installation, to install it in the ~/.local. Indeed, your .local subdirectories are standard paths for operating systems using systemd1.

So far so good, everything is compiled and installed. Let’s try out some features of the package :

mylogin@mymachine:~$ quantize-example-001 
mylogin@mymachine:~$ quantize-gui 

When testing the GUI, you can open an image and process it, for example using the image ~/.local/share/quantize/clint.jpeg :

The quantize gui compressing an image
The quantize gui compressing an image

Let’s have a look to the documentation

mylogin@mymachine:~$ firefox ~/.local/share/doc/quantize/html/index.html &

And finally, since we have a library that is installed on the system, we can make use of it to compile an executable from outside the source dirs :

mylogin@mymachine:~$ export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:~/.local/lib/pkgconfig/
mylogin@mymachine:~$ pkg-config --libs --cflags quantize 

For example, you could take process_clint.cpp, compile and run it :

mylogin@mymachine:~$ g++ -o process_clint process_clint.cpp -O3 $(pkg-config --libs --cflags quantize)
mylogin@mymachine:~$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:~/.local/lib
mylogin@mymachine:~$ ./process_clint
mylogin@mymachine:~$ ./process_clint ~/.local/share/quantize/clint.jpeg

One last feature we see in the very last part of this tutorial is for generating deb packages you can install on your distribution:

mylogin@mymachine:~/quantize/build/$ make packages

That makes a debian package quantize-dev.1.15-x86_64.deb you could install with dpkg for example.

Let’s now see the magic behind. To see how that works, you will be requested to navigate within the files provided by the quantize package. Do not panic with, this is a fully fledged package, there are a lot of things you do not know within the CMakeLists.txt and we will decipher every single word together.

Structure of the package

The package structure is given below :

quantize/                  <-- Our root directory
+-- CMakeLists.txt         <-- Our root CMakeLists.txt
+-- src/                   <-- Stores the C++ sources/headers of our package
|   +-- CMakeLists.txt     
|   +-- *.hpp *.cpp        <-- Our C++ source/header files
+-- examples/              <-- Stores C++ usage examples of our package
|   +-- CMakeLists.txt 
|   +-- *.cpp              <-- Some C++ example usage sources
+-- tests/                 <-- Stores the unit tests sources
|   +-- CMakeLists.txt
|   +-- test*.cpp          <-- Some C++ unitary tests
+-- doc/                   <-- Stores the documentation
|   +-- CMakeLists.txt
|   +-- Doxyfile.in        <-- For generating the doc with Doxygen
|   +-- images/            <-- some additional files required for the doc
+-- share/                 <-- Stores additional files
|   +-- CMakeLists.txt     
|   +-- ui/                <-- some files for the GUI
|   +-- clint.jpeg         <-- an example image for testing the package

From a broad perspective, each CMakeLists serves its own purpose:

Our project requires C++17, Opencv>=3.0 and optionally gtkmm-3.0. If gtkmm-3.0 is available on your system, we compile the GUI, otherwise we don’t.

General setup of the project

Everything starts with the root CMakeLists.txt file. Open it. A bare minimal CMakeLists.txt should contain the following :

cmake_minimum_required(VERSION 3.10)

project(quantize
        VERSION 1.51)

The last call defines the variables CMAKE_PROJECT_NAME and PROJECT_VERSION that we will make us of in the GUI, documentation and prefix for the executables and examples.

You have seen from the compilation of the project that we issued cmake .. from the build directory. The .. referred to the root directory of the package so that CMake processed the root CMakeLists.txt. Since we setup one CMakeLists per directory, we must propagate the cmake call to these subdirectories, which is done by

add_subdirectory(src)
add_subdirectory(share)
add_subdirectory(doc)
add_subdirectory(examples)

Within CMakeLists, various variables help you to define paths either to the source directory or the build directory:

Reminder on the compilation line with g++

Now we move on the question of how to tell CMake to generate the right compilation lines. As a reminder, for building an executable with g++, the commande line looks like :

mylogin@mymachine:~$ g++ -o sample source1.cpp source2.cpp -03 -std=c++17 -I/path/to/include1 -I/path/to/include2 -L/path/to/lib -lonelib -lanotherlib -Da_definition 

This line contains :
- optimization flags, for example-O3 for optimized code or -O0 -g for debug - include directories -I/path/to/include1 to look for header files not in standard paths
- library directories -L/path/to/lib to look for libraries not in standardp aths
- libraries -lonelib -lanotherlib for linking against libonelib.so and libanotherlib.so
- definitions -Da_definition for controlling the preprocessor
- g++ command line options -std=c++17 for enabling C++17 support

For building a shared library, it’s pretty similar, you just need to add a -shared:

mylogin@mymachine:~$ g++ -shared -o libsample.so source1.cpp source2.cpp -std=c++17 -I/path/to/include1 -I/path/to/include2 -L/path/to/lib -lonelib -lanotherlib -Da_definition 

Building and installing a shared library

Looking for dependencies

When your project depends on external libraries, you know that you need to specify them in the compilation line. For example, for compiling/linking a program sample.cpp depending on OpenCV, you need to specify both include directories (the -I) as well as the libraries against which to build :

mylogin@mymachine:~$ g++ -o sample sample.cpp -I/usr/include/opencv -lopencv_shape -lopencv_stitching -lopencv_superres -lopencv_videostab -lopencv_aruco -lopencv_bgsegm -lopencv_bioinspired ... 

This is quite cumbersome. Fortunately, OpenCV provides a pkg-config file. The flags can thus be obtained easily by this:

mylogin@mymachine:~$ pkg-config --libs --cflags opencv

but, instead of copy-pasting it in your command line, you can write this inside a $(...) statement, so that the command interpreter calls it and puts the result at that place.

mylogin@mymachine:~$ g++ -o sample sample.cpp $(pkg-config --libs --cflags opencv)

This introduction being done, there exists two mechanisms in CMake for looking for dependencies and collecting the compilation and linking flags into variables : pkg_check_modules and find_package.

Within our project, both the C++ sources in the examples directory and the library in the src depend on opencv. That’s a dependency we will therefore look in the root CMakeLists. In the root CMakeLists.txt, you will see :

find_package(PkgConfig)

pkg_check_modules(OPENCV REQUIRED opencv>=3.0)

The find_package(PkgConfig) command adds the pkg_check_modules and pkg_search_module commands. We then look for opencv. By specifying REQUIRED, cmake will stop if opencv is not found. Note we can specify a minimal version. If opencv is found, several cmake variables are defined, see the pkg_check_modules documentation for a full list, among which ${OPENCV_INCLUDE_DIRS} and ${OPENCV_LIBRARIES} which contain exactly the -I ... and -l.... Since that dependency is looked for in the root CMakeLists, the OPENCV_INCLUDE_DIRS and OPENCV_LIBRARIES will be available in the CMakeLists included by the root CMakeLists; basically in all the CMakeLists of our package.

If you want to know which packages can be found by pkg-config, check :

mylogin@mymachine:~$ pkg-config --list-all

If you want to know which packages can be found by find_package, check the /usr/share/cmake-3.10/Modules/ directory (adapt with your cmake version). Looking into the find scripts, you know which variables are defined.

Building the shared library

The shared library libquantize.so is built from quantize-cv.cpp. If you were to compile it from the command line, invoking manually g++, that would be :

mylogin@mymachine:~/quantize/src/$ g++ -shared -o libcvquantize.so quantize-cv.cpp -I. $(pkg-config --libs --cflags opencv) -O3 -std=c++17 -fPIC

The C++17 support is activated in the root CMakeLists.txt :

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

The optimization flags are given in the root CMakeLists.txt as well:

set(CMAKE_BUILD_TYPE Release)
# set(CMAKE_BUILD_TYPE Debug)

Now for defining the rules for compiling/linking the library, look in the quantize/src/CMakeLists.txt file.

add_library(cvquantize
            SHARED
            quantize-cv.cpp)

What we aim to process is called a target. Here, the target, i.e. the library, is cvquantize. You have to use this name for specifying complementary instructions about this target. This is why we invoque target_****(cvquantize, ...) commands, for the include directories and libraries:

target_include_directories(cvquantize PUBLIC ${OPENCV_INCLUDE_DIRS})
target_link_libraries(cvquantize ${OPENCV_LIBRARIES})

If you have to add library directories -L/path/to/lib, that is done with the link_directories command. For OpenCV, the libraries are in standard paths which explain why we did not have to specify them.

mylogin@mymachine:~$ locate libopencv_core.so
/usr/lib/x86_64-linux-gnu/libopencv_core.so

Actually, this is not completely true. Indeed, on MacOs, the OpenCV and gtkmm libraries are installed in paths not in the standard linker paths. So, for MacOs, we need to specify it and for other systems, these libs are installed in standard paths and there is no need to specify them. In CMake, we can filter based on the OS but as it is not hurting to add link directories that are useless and this is a simple solution in the context of this tutorial, we will follow that track. At the top of the CMakeLists.txt, we add:

link_libraries(${OPENCV_LIBRARIES} ${GTKMM_LIBRARIES})

note that on recent cmake versions (higher than 3.13.5) we can also restrict that call to specific targets with target_link_directories

Installing the library and the header files

For installing our library, we need to install both the shared library and the header files. Installation of the shared library is done with the install command :

install(TARGETS cvquantize
        DESTINATION lib)

This will install the libcvquantize.so file in the ${CMAKE_INSTALL_PREFIX}/lib directory. Even if we only specify lib in the DESTINATION, a relative path is always implicitely prefixed by CMAKE_INSTALL_PREFIX. So, in our case, the library got installed in ~/.local/lib/. Have a look in this directory and check that we are right.

For the header files, we have several files that all end up with .hpp. We can use globbing for easily gathering them and then install them all at the right place. Note that we are collecting only the headers which our library needs (not the gui-application-window.hpp for example) which are all stored in the quantize/src directory.

file(
    GLOB 
    headerfiles 
    quantize*.hpp
    )

install(FILES ${headerfiles}
        DESTINATION include/${CMAKE_PROJECT_NAME})

The file command, in the GLOB mode, will look for all the headers and store them in the list variable ${headerfiles} that can then be used for the install command. We also install these examples in the include/quantize directory; a relative install destination is always implicitely prefixed by CMAKE_INSTALL_PREFIX so that in the case of our installation we did before, that path is actually ~/.local/include/quantize. Later on, for any C++ file depending on our library, we would use includes like

#include <quantize.hpp>
//#include <quantize-cv.hpp>

If you want to see the content of the headerfiles variable, you can add, in the quantize/src/CMakeLists.txt file :

message("The content of the headerfiles variable is : ${headerfiles}")

If you rebuild (i.e. cmake ..), you will see the message.

Generating a pkgconfig file

In order to help other projects if they need to link against our library, we want to define a pkg-config file (as opencv did). The file can be generated from within the CMakeLists file directly using the file command. We use it to write the file quantize.pc within the build directory : that’s the meaning of ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.

file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.pc
    "
Name: ${CMAKE_PROJECT_NAME}
Description: A generic C++ implementation of K-means with examples using OpenCV and gtkmm3
Version: ${PROJECT_VERSION}
Requires: opencv >= 3.0
Libs: -L${CMAKE_INSTALL_PREFIX}/lib -lcvquantize
Cflags: -I${CMAKE_INSTALL_PREFIX}/include/${CMAKE_PROJECT_NAME} -std=c++17
    "
)

install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.pc
    DESTINATION lib/pkgconfig)

The pkg-config file being generated by the CMakeLists itself, it will be located in the build subdirectory hosting the CMakeList in which this file command written. In this case, it is the build directory directly so that we could have written CMAKE_BINARY_DIR but it is a more generally applicable to use of CMAKE_CURRENT_BINARY_DIR. The pkg-config file is installed in the lib/pkgconfig directory (which is a standard relative path, see for example the content of /usr/lib/pkgconfig). A relative destination path is always implicitely prefixed by CMAKE_INSTALL_PREFIX so that, in the case of our example installation above, that path is ~/.local/lib/pkconfig/. Check it !

To test it, just execute the code below :

mylogin@mymachine:~$ export PKG_CONFIG_PATH=~/.local/lib/pkgconfig && pkg-config --libs --cflags quantize
mylogin@mymachine:~$ export PKG_CONFIG_PATH=~/.local/lib/pkgconfig && pkg-config --modversion quantize

You are expected to see the compile/link flags required by our lib (including the opencv ones because libcvquantize.so depends on it) and the version of the package as stated in the CMakeLists.

Compiling an executable

Compiling and installing the GUI

To demonstrate how to build executables, we will see two examples. The first in the quantize/src/CMakeLists.txt and the second in the quantize/example/CMakeLists.txt

In the src directory, we will build the GUI under the condition that gtkmm-3.0 is available on your system. The GUI executable is built from several source files which are all prefixed by gui : gui-application-window.cpp and gui-main.cpp. So, we begin by looking for gtkmm with pkg_check_modules

pkg_check_modules(GTKMM gtkmm-3.0>=3.22)

which makes available the GTKMM_FOUND variable stating if gtkmm is available or not and, if available, defines GTKMM_LIBRARIES, GTKMM_INCLUDE_DIRS. For gtkmm-3.0, the libraries are installed in standard paths so that we do not need to bother about library directories. So, the code is basically :

if(GTKMM_FOUND)
    file(
        GLOB 
        guifiles    
        gui-*.cpp
    )

    add_executable(gui ${guifiles})
    target_include_directories(gui PUBLIC ${GTKMM_INCLUDE_DIRS})    
    target_link_libraries(gui cvquantize ${GTKMM_LIBRARIES} pthread)

    install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/gui 
            DESTINATION bin
            RENAME ${CMAKE_PROJECT_NAME}-gui)
endif()

We already know most of the commands. One note is the RENAME option of the install command which is going to rename the executable from gui to quantize-gui once installed. We also install these examples in the bin directory, but keep in mind that these relative destination path are always implicitely prefixed by CMAKE_INSTALL_PREFIX.

Finally, there is one line in the CMakeLists you are reading I did not speak about yet :

    target_compile_definitions(gui PUBLIC QUANTIZE_SHARE="${CMAKE_INSTALL_PREFIX}/share/${CMAKE_PROJECT_NAME}/")

This line is adding definitions in the compilation line. With CMAKE_INSTALL_PREFIX=~/.local/, it is providing the command line option -DQUANTIZE_SHARE=~/.local/share/quantize/. If you look in gui-main.cpp, the QUANTIZE_SHARE macro is used for accessing to the GUI glade file ui/windows.glade. That file will be installed by the quantize/share/CMakeLists.txt.

Compiling and installing the examples

For compiling the examples, we follow the same principle than for building the GUI, except that we automatize a little more the process. For automatizing the definition of the rules, we collect all the examples using the file command in the GLOB mode. We desitinguish two collection of examples, the ones that need to be linked with our library libquantize.so and the ones which don’t. All the examples example-*.cpp only use the C++ templates, without requiring the OpenCV library, and they include quantize.hpp. The examples cv-example-*.cpp include quantize-cv.hpp and need to be linked against our library.

Let us begin with the examples that do not require to be linked against our library and opencv. We first collect them :

file(
    GLOB 
    usage_examples
    example*.cpp
)

And we then need to define one add_executable per example. That can be done with the foreach loop of CMake.

foreach(f ${usage_examples})
  get_filename_component(exampleName ${f} NAME_WE) 

  add_executable (${exampleName} ${f}) 

  target_include_directories(${exampleName} PUBLIC ${CMAKE_SOURCE_DIR}/src)
 
  install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/${exampleName}
          DESTINATION bin
          RENAME ${CMAKE_PROJECT_NAME}-${exampleName})

endforeach(f)

This is almost self-explanatory. The first new thing is the get_filename_component(.... NAME_WE) which allows to get the basename of the file, i.e. without the extension (WE), for example example-001 if the filename f is example-001.cpp. The result is stored in exampleName and used for defining the name of the executable. In the install command, you should note that there is a PROGRAMS which is stating that the installed file should be have the owner, group and world permissions to be executed. We also install these examples in the bin directory, but keep in mind that these relative destination path are always implicitely prefixed by CMAKE_INSTALL_PREFIX.

The second set of examples are the ones which require to be linked against our lib which is done with :

    target_link_libraries(${exampleName} cvquantize)

There is one additional thing which is the call to target_compile_options. That command allows to provide so-called definitions to the compiler (some -DQUANTIZE_SHARE=..). In our case, that allows to replace a preprocessor option in the code, the path to the installed share directory. That depends on what the user defined for the CMAKE_INSTALL_PREFIX so that it cannot be hardcoded in the cpp file.

    target_compile_definitions(${exampleName} PUBLIC QUANTIZE_SHARE="${CMAKE_INSTALL_PREFIX}/share/${CMAKE_PROJECT_NAME}")

Installing additional files

Let’s move into the share subdirectory and look at its CMakeLists file. We need to install the files located in the share subdirectory :

This is almost only standard calls to the install command :

install(FILES clint.jpeg 
        DESTINATION share/${CMAKE_PROJECT_NAME})

There are actually additional steps in the share/CMakeLists.txt for the ui/window.glade file that I included for showing an additional feature of CMake. If you run the gui by calling quantize-gui and click on the About button, you should see the version number is written. That field is filled from the information provided in the root CMakeLists, with the variable PROJECT_VERSION. For injecting the content of a CMake variable in a file, in CMake words, we can configure_file a file :

configure_file(ui/windows.glade ${CMAKE_CURRENT_BINARY_DIR}/ui/windows.glade)

The configure_file command will substitute every ${VARNAME} (or @VARNAME@ if you use @ONLY), where VARNAME is a valid variable defined by CMake processing your CMakeLists and write the result in ${CMAKE_CURRENT_BINARY_DIR}/ui/windows.glade. Here, the xml file ui/windows.glade contains a ${PROJECT_VERSION} string, that string will be replaced by the value of that CMake variable.

Once the file is configured, we can install the configured file located in ${CMAKE_CURRENT_BINARY_DIR}/ui/windows.glade (not the original one located in ${CMAKE_CURRENT_SOURCE_DIR}/ui/windows.glade):

install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ui/windows.glade
        DESTINATION share/${CMAKE_PROJECT_NAME}/ui)

Generating a documentation

We now move into the doc subdirectory and look at its CMakeLists file. The documentation is generated using Doxygen. So we start by looking if Doxygen is available on the system, if not we do not compile the doc:

find_package(Doxygen)

if(NOT DOXYGEN_FOUND)
    message("Doxygen not found, I will not generate/install the documentation")
else()
    ...
endif()

The find_package(Doxygen) makes available various CMake variables and commands among which DOXYGEN_FOUND, DOXYGEN_EXECUTABLE, .. (have a look to the FindDoxygen.cmake file located in the /usr/share/doc/cmake-3.10/Modules/FindDoxygen.cmake file).

Doxygen requires a configuration file Doxyfile. If you look in the doc/Doxyfile.in file, it contains CMake variables CMAKE_PROJECT_NAME, PROJECT_VERSION, CMAKE_SOURCE_DIR, .. all of them need to substituted when the project is compiled. This is done with the configure_file command we have seen before.

    configure_file(Doxyfile.in Doxyfile)

The configured file will be located in ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile. We then define a target named doc with the add_custom_target command.

    set(DOXYGEN_INPUT ${CMAKE_BINARY_DIR}/doc/Doxyfile)

    # We specify the command used by doxygen to build the doc
    add_custom_target(doc ALL
                      COMMAND ${CMAKE_COMMAND} -E echo_append "Building API Documentation..."
                      COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_INPUT} > /dev/null
                      COMMAND ${CMAKE_COMMAND} -E echo "Done."
        )

This target can be issued by the makefile, from the buildir by calling make doc. The ALL option tells CMake to build that target every time, without having to explicitely call make doc.

The last step is to install the documentation:

    install(DIRECTORY ${CMAKE_BINARY_DIR}/doc/html 
            DESTINATION share/doc/${CMAKE_PROJECT_NAME})

In our case, that will be ~/.local/share/doc/quantize. Check this out !

Unitary tests

When unit tests in CMake, we need to enable it in the root CMakeLists with the enable_testing command :

enable_testing()
add_subdirectory(tests)

This will give you access to the add_test command that we use later on.

Let us now move into unit tests defined in the quantize/tests. For defining unitary tests, we use the Boost unit test framework. In the quantize/tests/CMakeLists.txt file, we first look for that dependency:

find_package (Boost COMPONENTS unit_test_framework REQUIRED)

We then collect all the unit test sources (remember the file command in the GLOB mode) and define the compilation (remember the foreach command) for it :

file(
    GLOB 
    testfiles
    test*.cpp
)

foreach(f ${testfiles})
  get_filename_component(testname ${f} NAME_WE) 

  add_executable(${testname} ${f}) 

  target_include_directories (${testname} 
      PUBLIC ${CMAKE_SOURCE_DIR}/src ${Boost_INCLUDE_DIRS})

  target_compile_definitions(${testname} PUBLIC BOOST_TEST_DYN_LINK)

  target_link_libraries(${testname} 
      ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}
  )

  # And then register the example within the test suite
  add_test(NAME ${testname}
           COMMAND ${testname})
endforeach(f)

For linking with the Boost Test libraries, we need to add the definition -DBOOST_TEST_DYN_LINK. The other commands, we know them already. The only new command is the add_test which is registering the executable into the collection of tests CMake is aware of. All the tests registered with add_test are then called when invoking the make test command from the build directory.

Oh, and by the way, you can run the tests with make test or indiviually every test executable. These executables also accept command line arguments, check build/tests/test_vector --help. Run with cd build; ./tests/test_vector --log_level=all

More advanced features

Which are the variables defined in CMake

In the root CMakeLists quantize project, we added at the end of the file, some code to display the variables defined by CMake :

if(NOT DISPLAY_CMAKE_VAR)
    set(DISPLAY_CMAKE_VAR FALSE)
endif()
if(DISPLAY_CMAKE_VAR)
    get_cmake_property(_variableNames VARIABLES)
    foreach (_variableName ${_variableNames})
        message(STATUS "${_variableName}=${${_variableName}}")
    endforeach()
endif()

This can be triggered by running :

mylogin@mymachine:~/quantize/build/$ cmake .. -DDISPLAY_CMAKE_VAR=True

That can be usefull when you are confused about what is defined or not.

Making a source archive

If you want to build an archive from your sources, we can create a custom target

SET(DIST_DIR "${CMAKE_PROJECT_NAME}")
ADD_CUSTOM_TARGET(dist 
    COMMAND rm -rf ${DIST_DIR}
    COMMAND mkdir  ${DIST_DIR}
    COMMAND cp -r ${CMAKE_SOURCE_DIR}/* ${DIST_DIR} || true 
    COMMAND rm -rf ${DIST_DIR}/build
    COMMAND mkdir ${DIST_DIR}/build
    COMMAND tar --exclude="*~" --exclude="._*" -zcvf ${DIST_DIR}-${PROJECT_VERSION}.tar.gz ${DIST_DIR}
    COMMAND rm -rf  ${DIST_DIR}
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR})

From the build dir, you can then :

mylogin@mymachine:~/quantize/build/$ make dist

and you then get your quantize-${PROJECT_VERSION}.tar.gz archive.

Building up DEB, RPM, DMG, EXE packages

We can make an installable package (.deb, .rpm, .dmg, .exe) by using CPack

SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "C++ application for demonstrating Kmeans clustering")
SET(CPACK_PACKAGE_VENDOR "Jeremy Fix")
SET(CPACK_PACKAGE_CONTACT "{Jeremy_DOT_fix_AT_centralesupelec.fr")
SET(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
SET(CPACK_PACKAGE_LICENSE "GPL")
SET(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_SOURCE_DIR}/LICENSE)
SET(CPACK_RESOURCE_FILE_README ${CMAKE_SOURCE_DIR}/README.md)
SET(CPACK_RESOURCE_FILE_WELCOME ${CMAKE_SOURCE_DIR}/README.md)

ADD_CUSTOM_TARGET(packages
          COMMAND rm -rf build
          COMMAND mkdir build
          COMMAND cd build && cmake ${CMAKE_SOURCE_DIR} -DCMAKE_INSTALL_PREFIX=/usr && make package -j
          COMMAND cp build/*.deb . || true
          COMMAND rm -rf build
          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)

###############################################
# For DEBs
###############################################
find_program( DPKGDEB
     NAMES dpkg-deb
     PATHS "/usr/bin"
)
IF(NOT DPKGDEB STREQUAL  "DPKGDEB-NOTFOUND")
  MESSAGE("Set up for building DEB")

  SET(CPACK_DEBIAN_PACKAGE_NAME ${CMAKE_PROJECT_NAME}-dev)
  SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libgtkmm-3.0-dev (>=  3.22), libopencv-core-dev (>= 3.0)")

  SET(CPACK_PACKAGE_FILE_NAME ${CPACK_DEBIAN_PACKAGE_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_PROCESSOR})

  SET(CPACK_DEBIAN_PACKAGE_MAINTAINER ${CPACK_PACKAGE_CONTACT})
  SET(CPACK_GENERATOR "DEB")
ENDIF(NOT DPKGDEB STREQUAL  "DPKGDEB-NOTFOUND")

###############################################
INCLUDE(CPack)

That’s a basic setup for making a deb package. You could do something similar for building exe installer for windows, dmg installer for MacOS, rpm installer for Fedora/RedHat, …

You could also set up a binary package and devel package. For this, you would have to look at the COMPONENT attribute of the install commands and then define appropriately the CPACK_INSTALL_CMAKE_PROJECTS variable.


  1. For more information, see man file-hierarchy

Jeremy Fix,