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
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.
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 !
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
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
:
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.
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.
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:
CMAKE_SOURCE_DIR
: that’s the root directory of your sources, in our case quantize
CMAKE_BUILD_DIR
: that’s the directory from which we called the cmake ..
, in our case quantize/build
CMAKE_CURRENT_SOURCE_DIR
: that’s the source directory in which the CMakeLists, accessing this variable, is defined. For example, within quantize/share/CMakeLists.txt
, that variable holds quantize/share
CMAKE_CURRENT_BUILD_DIR
: that’s the build directory associated with the source directory in which the CMakeLists is accessing this variable. For example, in our case, within quantize/share/CMakeLists.txt
, that variable holds quantize/build/share
.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
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.
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
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.
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.
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
.
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}")
Let’s move into the share
subdirectory and look at its CMakeLists file. We need to install the files located in the share
subdirectory :
ui/window.glade
GUI file used by gtkmmclint.jpeg
example imageThis 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)
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 !
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
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.
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.
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.
For more information, see man file-hierarchy
↩