Effective Cmake

broken image


I have used HPCToolkit and VTune and they are very effective at finding the long pole in the tent and do not need your code to be recompiled (except that you have to use -g -O or RelWithDebInfo type build in CMake to get meaningful output). I have heard TAU is similar in capabilities. Use the CMAKEINSTALLMESSAGE variable to control which messages are printed. New in version 3.11: Many of the install variants implicitly create the directories containing the installed files. If CMAKEINSTALLDEFAULTDIRECTORYPERMISSIONS is set, these directories will be created with the permissions specified.

CMake is a family of tools designed to build, test and package software. CMake is used to control the software compilation process using simple platform and compiler independent configuration files. CMake generates native makefiles and workspaces that can be used in the compiler environment of your choice.1 13 5. See Daniel Pfeifer's CNow 2017 talk Effective CMake (slide 31ff.) for more information. Functions and Macros Prefer functions over macros whenever reasonable. In addition to directory-based scope, CMake functions have their own scope. This means variables set inside functions are not visible in the parent scope. This is not true of macros. If you use multiple -O options, with or without level numbers, the last such option is the one that is effective.

Abstract

I am sure that every C++ programmer has at one point struggled with CMake. There have been multiple times where when I have to start work on some C++ project, I've to spend a good couple of hours in thinking how should my project structure look like. It's just such a huge hassle to think about all of your CMakeList.txt files and possible libraries and different modules and… the things that may go wrong with your build system. I've been meaning to find a good template of a CMake project for a long time and now I think I've foundcreated my long wanted gem. In this post, I first want to give a brief overview of my experience with CMake and the present the template project that I have finally settled with.

The horrible past experience

I have bad memories in my early days of dealing with CMake. At the start, I had the impression that a build system should make everything as easy as possible but it seems that often this is not the case. Don't get me wrong. I know build systems do not solve an easy problem, but still. I quickly noticed how there are a lot of people on the internet talking about their way of doing CMake projects. It didn't seem that there wasn't the way of doing it. This is generally a good thing. Freedom and doing things your way! But everything comes with a cost. The cost of CMake – you have no idea what you are supposed to do at the beginning.

My first experience with a big CMake project was while dealing with legacy code. The structure was similar to the one of OpenCV. A big CMakeList.txt file at the top lever with general project settings, a separate file with all of the libraries needed for the project and then a source directory with different modules. Each module is in a folder of its own. The structure of a module: source, include, test, and data directory; a CMakeList.txt file a the root of the module to define all executables and libraries. In all fairness, I still like the structure of the individual modules. The strange thing with the module-system was the way it did dependency resolution. Everything was done 'manually'. It essentially was a big CMake framework that first collected info about every module, looked at the dependencies between them and the needed libraries and then build targets manually for a module by linking explicitly everything that module needed. The whole thing was implemented in a collection of complicated macros. There maps and lists and algorithms and directory traversals and everything you can imagine. The whole thing is written in CMake of course. Legacy code for the win!

At one point I was considering building a similar system myself for my personal projects. Needless to say, that didn't take off. I wanted to clear up the structure and tightened it around the edges but I didn't have any form of success.

I browsed some more CMake projects, I watched some talks (see Effective CMake Talk by Daniel Pfeifer) and I also tried setting up some thins myself. Nothing adequately clicked with me. CMake was still getting in the way of my C++ programming and it was making it even more painful than it already is. This went on for some while.

The bright new world

Finally, a very fortunate thing happened. I watched Applied Best Practices and decided to check out the repository. The project is created with the idea of being a demonstration of 'doing things properly' in C++. I can agree with this sentiment 100%! I still did some adjustments but I am still grateful that I found this project. Here I will go over the elements of the structure of the template project that I created.

Conan

Conan is one of the numerous attempts to bring the 'package management world' to C++. Package management is one of those things that modern languages just 'have by default' but somehow C++ hasn't exactly caught up. For by CMake structure I wanted at least some attempt to make external dependency management a little bit easier. For this reason, I figured that Conan may be the utility to start my experimentation with 'C++ package management'

From Conan's website:

The open-source, decentralized and multi-platform package manager to create and share all your native binaries.

I won't give (because I can't) a comprehensive guide on Conan here. I believe this here is just enough to set you up and give you some basics of how Conan does its magic.

Conan can be installed through pip and there is an official get started guide. Once everything is set up properly, you can write your conanfile.txt file in the root directory of your project. This is where all of your dependencies are defined. An example file is:

It is a conf file with several sections. The dependencies go in the requires node. Usually what to put there is defined on the page of each Conan package. The generators section defines exactly what Conan does and how it builds the dependencies configuration files. Different generators have different effects and use cases. As we are dealing with a CMake project, the cmake and cmake_find_package are enough. With this configuration setup, you can later include several lines of CMake code in your top-level CMakeLists.txt and just call find_package(..) for your dependencies. In theory at least. Sometimes things are a little bit tricky to get them running. If everything is as it is supposed to be, you can execute conan install in the build directory of the project and all of the dependencies will be fetched and saved in some cache on your machine.

In my top-level CMakeLists.txt, I have the following lines:

This sets up everything and at some later point I can just use something like:

This includes all of the CMake targets defined for Clara. We can later effortlessly link agings them when defining our executable for example.

Top level CMakeList.txt

With Conan out of the way, I can now proceed to the pure CMake part of my project structure. At the start, I want to layout my key core design goals.

  • Follow modern CMake guidelines
  • Ease of use
  • Modular setup
  • Not worrying about dependencies between modules
  • Support for testing
  • Support for automatics documentation generation

Folder structure

Effective Cmake Analysis

Effective Cmake

A high-level overview of the my project structure is as follows:

Everything comes together at the top-level CMakeLists.txt. This is the main entry point when running cmake . In the cmake several utility CMake scripts solve several small problems like finding the git version of the host machine and preventing me to build the project inside the source file tree. The Doxyfile.in file contains the basic configuration setup for Doxygen which I use for generating documentation. My idea for the libs folder is to encapsulate the external dependencies that are not available through Conan. Of course, for some big dependencies (e.g. OpenCV) this is not really viable but it works like a charm for header-only libraries. The libs/CMakeLists.txt is responsible for loading the libraries in the libs folder. The src directory houses all of the individual sub-modules in separate sub-folders that are included through the src/CMakeLists.txx script. More on the individual modules in a minute. In templates/ I have template visions of the two modules that the project can have – a library or an executable. I want to add a new module to the project, I can use of the scrips at top-level – create_new_app_module.sh or create_new_lib_module.sh – to generate the module easily. The create_new_app_module.sh looks like this:

With this setup, I can simply execute ./create_new_app_module.sh executable_1 to add a new sub-module with the name 'executable_1'.

The Makefile is there just so that I can automate some of the things I do regularly in the project folder. Things like rebuilding, creating debug or release builds or cleaning all build folders.

Cmake

Options

With the general folder structure, we can now go through several parts of the top-level CMakeLists.txt script.

Near the top of the script, I have the options with which the project can be built. Those are just variables that can be true or false and enable certain conditions for the later parts of the scripts. The options are:

I think the help strings are pretty self-explanatory so I won't go over each option individually. If you see any of these variables in later snippets, just know that it can be adjusted through the way the cmake is called when building the project. The options are passed as -D arguments to the cmake command. For example, to build with DEBUG_LOGGING enabled, we must call cmake like:

Cmake

Targets

Modern CMake is all about targets! The general rule of thumb is not to touch any variable in CMake (like CMAKE_CXX_FLAGS) directly but rather impose some requirements on a certain target. For the most part, the top-level CMakeLists.txt follows this paradigm. Targets can be defines as INTERFACE. This means that they don't produce any build output (neither library nor executable) but rather exist purely to be dependencies of other targets. Interface targets can be used, for example, to 'contain' compile options. When an executable target is defined and it links against one such interface target, all of the compiler options imposed on the interface will also be imposed on the executable. This is my general idea that the CMakeLists.txt is structured around.

At the start, there are two INTERFACE targets defined – project_warnings and project_options

project_warnings is meant to keep track of the flags that instruct the compiler on what warning to report on. project_options is for every other flag that may be passed to the compiler.

Flags

After the definition of the targets, several checks decide on the compiler flags that are to be used. The significant parts of the 'building' of both targets are given in the following snippets.

For starter, we make sure that we are programming in C++17. C++17 is as good as it gets and it's the current year so, of course, we are going to use it for every personal project.

If coverage is enabled for the build, we set the appropriate flags for the compiler. Notice how we are defining the options only on the targets and not in 'global scope' through the CMAKE_CXX_FLAGS flag.

We do something similar for the address sanitizers of the compiler:

While developing in C++, warnings are your friend. The more the better! Warnings can expose lots of tiny mistakes that you can make while writing C++ and in this sense, the compiler is your friend. As long as you tell it to report on the proper warning, of course.

A colorful output on the terminal is always useful.

Effective Cmake Training

Extra tools

Other than the compiler flags, several other external tools can help you in your C++ development. Many of them can be integrated with CMake. In my project template, I have three of them.

CCache is a compiler cache that speeds up recompilation. It is a separate program and you have to have it installed on your system. If this is the case, the following snippet will set up ccache in our build.

Cppcheck is a static analysis tool for C++ code. I can help you catch some common mistakes while programming and it even doesn't require you to compile your code. If you have Cppcheck on your system and you've enabled it in your build, you can use it with CMake like this:

Clang-Tidy is yet another tool for static analysis of C++ code. It can catch different set ot errors than cppcheck. As with the warnings, the more things the tools can tell us about our code, the better. The CMake integration is, again, possible and trivial:

Configurable header

In some situations, in your C++ code, you'll need information that is available only in the CMake build. This may include build information, version of the project, endianness of the host system, a compile-time configuration of some sort, etc. Because of this, I figured out that I may need some sort of a 'global' header file where such things are saved by CMake and accessible in the C++ code. My approach is to have the file src/include//config.hpp.in that will be configured by CMake and every file in the project would be able to include it. On the CMake side of the things, the code looks like this:

Using a CMake function like include_directories is generally a bad practice but in this case, just for once, in a 'global context', I am okay with it. In the config.hpp.in the file we can use all of the variables in the CMake environment to define whatever we want in the C++ code. To access a CMake variable, we use the @@ syntax. For example, the file can look like something like:

Documentation building

One of my design goals for the project structure was to be able to handle building documentation. I achieve that through a custom target. In CMake we can specify a command to be executed (in the sense on a command on the terminal) and 'bind' is to a make target in the final Makefile in the build directory. This means that at the end of the build process, I can execute something like make doc in the build directory to build the documentation for the project. Setting up Doxygen in CMake is not complicated. There is a package that can be included through find_package. This sets several CMake variables that are relevant to Doxygen. The whole setup can be done like:

Summery

At the end of CMakeLists.txt, I've created a bunch of messages that show exactly hot the project is currently being build. It just prints out the options of the build and whether or not they are active or not.

Modules

Let's now look at the individual submodules and how are those organized. As said, the src/CMakeLists.txt script includes them all through several calls of the add_subdirectory function.

The folder structure of each module is the following:

The src/ directory is meant for the source files as well as the private headers of the module. 'Private headers' means that those won't be visible outside of the project. The public headers are meant to go in the include/ directory. There is, however, a small caveat to its subfolder structure. In order to keep everything in my project organize, so that I can keep my sanity, I prefer to include the headers files in the form of:

In my mind, this is the most 'logical' way to include something that is somewhere in the project as there is a clear hierarchy. Daz victoria 8. This can be achieved by having several subfolders in the include/ directory. The structure at the end is:

The CMakeLists.txt file for the modules is relatively simple. It just has to create a target (a library or executable), set up the include directories and then link it against the necessary other targets. In the top-level CMakeLists.txt, we've created the two INTERFACE targets project_options and project_warnings. Those are the 'mandatory' ones to link against and every other target can be a one from the external library andor a different submodule of the project. The whole CMakeLists.txt file looks like:

The last call to the install function instructs CMake on how to construct the install target of the final Makefile. The function call basically makes all artifacts 'go to the right place'. I think that the most relevant part is that the header files will be copied to the global include directory (e.g. /usr/include/) but the will be put inside a folder with the project's name. In this case, another project can include headers from this project like:

which again helps to keep things organized.

Testing

The final aspect we have to look at is how to enable support for running C++ tests. Thankfully, CMake makes the integration incredibly easy. CMake uses ctest to discover and run tests I write the tests themselves with Catch – a header-only, test framework for unit-tests. There is a nice startup guide for Catch here. The general idea is to create test executables with the help of Catch and then let ctest run them. To enable ctest support in a project its enough to call the enable_testing function in the top CMakeLists.txt file. In there I have:

In the CMakeLists.txt files, I conditionally add the test/ directory of each submodule. In there, there is a different CMakaLists.txt file that sets up the testing for the corresponding module. The script is nothing special and it is very similar to the upper-level one.

Cmake

A high-level overview of the my project structure is as follows:

Everything comes together at the top-level CMakeLists.txt. This is the main entry point when running cmake . In the cmake several utility CMake scripts solve several small problems like finding the git version of the host machine and preventing me to build the project inside the source file tree. The Doxyfile.in file contains the basic configuration setup for Doxygen which I use for generating documentation. My idea for the libs folder is to encapsulate the external dependencies that are not available through Conan. Of course, for some big dependencies (e.g. OpenCV) this is not really viable but it works like a charm for header-only libraries. The libs/CMakeLists.txt is responsible for loading the libraries in the libs folder. The src directory houses all of the individual sub-modules in separate sub-folders that are included through the src/CMakeLists.txx script. More on the individual modules in a minute. In templates/ I have template visions of the two modules that the project can have – a library or an executable. I want to add a new module to the project, I can use of the scrips at top-level – create_new_app_module.sh or create_new_lib_module.sh – to generate the module easily. The create_new_app_module.sh looks like this:

With this setup, I can simply execute ./create_new_app_module.sh executable_1 to add a new sub-module with the name 'executable_1'.

The Makefile is there just so that I can automate some of the things I do regularly in the project folder. Things like rebuilding, creating debug or release builds or cleaning all build folders.

Options

With the general folder structure, we can now go through several parts of the top-level CMakeLists.txt script.

Near the top of the script, I have the options with which the project can be built. Those are just variables that can be true or false and enable certain conditions for the later parts of the scripts. The options are:

I think the help strings are pretty self-explanatory so I won't go over each option individually. If you see any of these variables in later snippets, just know that it can be adjusted through the way the cmake is called when building the project. The options are passed as -D arguments to the cmake command. For example, to build with DEBUG_LOGGING enabled, we must call cmake like:

Targets

Modern CMake is all about targets! The general rule of thumb is not to touch any variable in CMake (like CMAKE_CXX_FLAGS) directly but rather impose some requirements on a certain target. For the most part, the top-level CMakeLists.txt follows this paradigm. Targets can be defines as INTERFACE. This means that they don't produce any build output (neither library nor executable) but rather exist purely to be dependencies of other targets. Interface targets can be used, for example, to 'contain' compile options. When an executable target is defined and it links against one such interface target, all of the compiler options imposed on the interface will also be imposed on the executable. This is my general idea that the CMakeLists.txt is structured around.

At the start, there are two INTERFACE targets defined – project_warnings and project_options

project_warnings is meant to keep track of the flags that instruct the compiler on what warning to report on. project_options is for every other flag that may be passed to the compiler.

Flags

After the definition of the targets, several checks decide on the compiler flags that are to be used. The significant parts of the 'building' of both targets are given in the following snippets.

For starter, we make sure that we are programming in C++17. C++17 is as good as it gets and it's the current year so, of course, we are going to use it for every personal project.

If coverage is enabled for the build, we set the appropriate flags for the compiler. Notice how we are defining the options only on the targets and not in 'global scope' through the CMAKE_CXX_FLAGS flag.

We do something similar for the address sanitizers of the compiler:

While developing in C++, warnings are your friend. The more the better! Warnings can expose lots of tiny mistakes that you can make while writing C++ and in this sense, the compiler is your friend. As long as you tell it to report on the proper warning, of course.

A colorful output on the terminal is always useful.

Effective Cmake Training

Extra tools

Other than the compiler flags, several other external tools can help you in your C++ development. Many of them can be integrated with CMake. In my project template, I have three of them.

CCache is a compiler cache that speeds up recompilation. It is a separate program and you have to have it installed on your system. If this is the case, the following snippet will set up ccache in our build.

Cppcheck is a static analysis tool for C++ code. I can help you catch some common mistakes while programming and it even doesn't require you to compile your code. If you have Cppcheck on your system and you've enabled it in your build, you can use it with CMake like this:

Clang-Tidy is yet another tool for static analysis of C++ code. It can catch different set ot errors than cppcheck. As with the warnings, the more things the tools can tell us about our code, the better. The CMake integration is, again, possible and trivial:

Configurable header

In some situations, in your C++ code, you'll need information that is available only in the CMake build. This may include build information, version of the project, endianness of the host system, a compile-time configuration of some sort, etc. Because of this, I figured out that I may need some sort of a 'global' header file where such things are saved by CMake and accessible in the C++ code. My approach is to have the file src/include//config.hpp.in that will be configured by CMake and every file in the project would be able to include it. On the CMake side of the things, the code looks like this:

Using a CMake function like include_directories is generally a bad practice but in this case, just for once, in a 'global context', I am okay with it. In the config.hpp.in the file we can use all of the variables in the CMake environment to define whatever we want in the C++ code. To access a CMake variable, we use the @@ syntax. For example, the file can look like something like:

Documentation building

One of my design goals for the project structure was to be able to handle building documentation. I achieve that through a custom target. In CMake we can specify a command to be executed (in the sense on a command on the terminal) and 'bind' is to a make target in the final Makefile in the build directory. This means that at the end of the build process, I can execute something like make doc in the build directory to build the documentation for the project. Setting up Doxygen in CMake is not complicated. There is a package that can be included through find_package. This sets several CMake variables that are relevant to Doxygen. The whole setup can be done like:

Summery

At the end of CMakeLists.txt, I've created a bunch of messages that show exactly hot the project is currently being build. It just prints out the options of the build and whether or not they are active or not.

Modules

Let's now look at the individual submodules and how are those organized. As said, the src/CMakeLists.txt script includes them all through several calls of the add_subdirectory function.

The folder structure of each module is the following:

The src/ directory is meant for the source files as well as the private headers of the module. 'Private headers' means that those won't be visible outside of the project. The public headers are meant to go in the include/ directory. There is, however, a small caveat to its subfolder structure. In order to keep everything in my project organize, so that I can keep my sanity, I prefer to include the headers files in the form of:

In my mind, this is the most 'logical' way to include something that is somewhere in the project as there is a clear hierarchy. Daz victoria 8. This can be achieved by having several subfolders in the include/ directory. The structure at the end is:

The CMakeLists.txt file for the modules is relatively simple. It just has to create a target (a library or executable), set up the include directories and then link it against the necessary other targets. In the top-level CMakeLists.txt, we've created the two INTERFACE targets project_options and project_warnings. Those are the 'mandatory' ones to link against and every other target can be a one from the external library andor a different submodule of the project. The whole CMakeLists.txt file looks like:

The last call to the install function instructs CMake on how to construct the install target of the final Makefile. The function call basically makes all artifacts 'go to the right place'. I think that the most relevant part is that the header files will be copied to the global include directory (e.g. /usr/include/) but the will be put inside a folder with the project's name. In this case, another project can include headers from this project like:

which again helps to keep things organized.

Testing

The final aspect we have to look at is how to enable support for running C++ tests. Thankfully, CMake makes the integration incredibly easy. CMake uses ctest to discover and run tests I write the tests themselves with Catch – a header-only, test framework for unit-tests. There is a nice startup guide for Catch here. The general idea is to create test executables with the help of Catch and then let ctest run them. To enable ctest support in a project its enough to call the enable_testing function in the top CMakeLists.txt file. In there I have:

In the CMakeLists.txt files, I conditionally add the test/ directory of each submodule. In there, there is a different CMakaLists.txt file that sets up the testing for the corresponding module. The script is nothing special and it is very similar to the upper-level one.

In the CMakeLists.txt files I conditionally add the test/ directrory of each submodule. In therem, there is a different CMakaLists.txt file that sets up the testing for the corresponding module. The script is nothing special and it is very similar to the upper level one.

The last three lines turn this into a 'test target'. The build will thus produce a binary that can be ran and all the tests defined in it will be executed. As ctest was enabled, the whole process boils down to executing make test in the build directrory of the project. submodule_1_test.cpp ./src/src_file_1.cpp ./src/src_file_2.cpp)

target_include_directories(${PROJECT_NAME}_alisp_test PUBLIC ./include PRIVATE ./src)

target_link_libraries(${PROJECT_NAME}_alisp_test PRIVATE project_options project_warnings PUBLIC Catch2::Catch2)

include(CTest) include(Catch) catch_discover_tests(submodule_1_test) #+END_SRC

The last three lines turn this into a 'test target'. The build will thus produce a binary that can be ran and all the tests defined in it will be executed. As ctest was enabled, the whole process boils down to executing make test in the build directory of the project.

Conclusion

So those are my two cents about CMake and project structure. I don't claim to have a lot of experience but I've done a lot of research in the past year and a half. I've looked into different projects, read the best practices, read a lot of vague tutorials on the internet and watched the relevant talks. I have thought this several times, but this time I really think I've nailed it. I hope that I've created (mostly stolen) something scalable that will serve me well in my future small to mid-size projects. Whether of not scalability should be of my concern is a completely separate matter 🙂.

References

Effective Cmake Meaning

  • [1] CPP_BOX is a project by Jason Turner.
  • [2] Victoria Rudakova's Post
  • [3] Effective CMake Talk by Daniel Pfeifer
  • [4] Applied Best Practices Talk by Jason Turner
  • [5] CMake's documentation

Effective Cmake Definition

I finished watching 'Effective CMake' talk by Daniel Pfeifer from last
year and it seems to me it is the 'GO TO' resource for best practices. A
quick scan of my CMakeLists.txt files and sure enough, I use
include_directories() and other 'dont's'. The problem is that none of
the things mentioned in the talk:
a) give any warnings when running cmake
b) are mentioned as bad practice in the docs
What I would prefer is that everytime a bad practice is used a big red
warning would be printed by CMake so I could see it and correct it.
Since CMake is apparently very slow deprecating things a solution
appeared in my mind after seeing the function wrap functionality. How
about a file called Effective.cmake which contains something like
(pseudocode):
function(include_directories input output)
message(DEPRECATION 'Use target_include_directories() instead.'
_include_directories(..)
endfunction()
function(set input output)
if (${ARG0} STREQUAL 'CMAKE_CXX_FLAGS')
message(DEPRECATION 'You probably shouldn't use this directly')
endif
_set(..)
endfunction()
..
then include(Effective.cmake) in your CMakeLists.txt to enable all
warnings. The effort here is to compile a list of existing bad practices
and wrap them (if such a thing is possible).
Does this approach seem reasonable? Is there an effort with similar
goals out in the wild which I should know about?
Best regards,
cen
--
Powered by www.kitware.com
Please keep messages on-topic and check the CMake FAQ at: http://www.cmake.org/Wiki/CMake_FAQ
Kitware offers various services to support the CMake community. For more information on each offering, please visit:
CMake Support: http://cmake.org/cmake/help/support.html
CMake Consulting: http://cmake.org/cmake/help/consulting.html
CMake Training Courses: http://cmake.org/cmake/help/training.html
Visit other Kitware open-source projects at http://www.kitware.com/opensource/opensource.html
Follow this link to subscribe/unsubscribe:
https://cmake.org/mailman/listinfo/cmake




broken image