CMake and C++ are brothers forever

CMake and C++ are brothers forever

During development, I like to change compilers, build modes, dependency versions, perform static analysis, measure performance, collect coverage, generate documentation, and so on. And I really love CMake because it allows me to do whatever I want.

Many scold CMake, and often deservedly, but if you look, it's not so bad, but lately not bad at all, and the direction of development is quite positive.

In this note, I want to tell you how easy it is to organize a C++ header library in a CMake system to get the following functionality:

  1. Assembly;
  2. Autorun tests;
  3. Code coverage measurement;
  4. installation;
  5. Autodocumentation;
  6. Online sandbox generation;
  7. Static analysis.

Who already understands the pros and si-make can just download project template and start using it.


Content

  1. Project from the inside
    1. Project structure
    2. Main CMake file (./CMakeLists.txt)
      1. Project Information
      2. Project Options
      3. Compilation Options
      4. The main goal
      5. Installation
      6. Tests
      7. Documentation
      8. Online sandbox
    3. Script for tests (test/CMakeLists.txt)
      1. The test is
      2. ΠŸΠΎΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅
    4. Documentation script (doc/CMakeLists.txt)
    5. Script for online sandbox (online/CMakeLists.txt)
  2. project outside
    1. Assembly
      1. Generation
      2. Assembly
    2. Options
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. assembly targets
      1. By default
      2. mylib-unit-tests
      3. check
      4. coverage
      5. doc
      6. wandbox
    4. Examples
  3. Tools
  4. Static Analysis
  5. Afterword

Project from the inside

Project structure

.
β”œβ”€β”€ CMakeLists.txt
β”œβ”€β”€ README.en.md
β”œβ”€β”€ README.md
β”œβ”€β”€ doc
β”‚   β”œβ”€β”€ CMakeLists.txt
β”‚   └── Doxyfile.in
β”œβ”€β”€ include
β”‚   └── mylib
β”‚       └── myfeature.hpp
β”œβ”€β”€ online
β”‚   β”œβ”€β”€ CMakeLists.txt
β”‚   β”œβ”€β”€ mylib-example.cpp
β”‚   └── wandbox.py
└── test
    β”œβ”€β”€ CMakeLists.txt
    β”œβ”€β”€ mylib
    β”‚   └── myfeature.cpp
    └── test_main.cpp

This will mainly focus on how to organize CMake scripts, so they will be analyzed in detail. The rest of the files can be viewed directly by anyone. on the template project page.

Main CMake file (./CMakeLists.txt)

Project Information

First of all, you need to request the desired version of the CMake system. CMake develops, command signatures change, behavior under different conditions. In order for CMake to immediately understand what we want from it, we need to immediately fix our requirements for it.

cmake_minimum_required(VERSION 3.13)

Then we denote our project, its name, version, languages ​​used, etc. ΠΊΠΎΠΌΠ°Π½Π΄Ρƒ project).

In this case, specify the language CXX (which means C++) so that CMake doesn't bother looking for a C compiler (by default, two languages ​​are included in CMake: C and C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Here you can immediately check whether our project is included in another project as a subproject. This will help a lot in the future.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Project Options

Let's consider two options.

The first option is MYLIB_TESTING - to disable unit tests. This may be necessary if we are sure that everything is in order with the tests, and we want, for example, only to install or package our project. Or our project is included as a subproject - in this case, the user of our project is not interested in running our tests. You don't test the dependencies you use, do you?

option(MYLIB_TESTING "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ ΠΌΠΎΠ΄ΡƒΠ»ΡŒΠ½ΠΎΠ΅ тСстированиС" ON)

In addition, we will make a separate option MYLIB_COVERAGE for measuring code coverage with tests, but it will require additional tools, so you will need to enable it explicitly.

option(MYLIB_COVERAGE "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ ΠΈΠ·ΠΌΠ΅Ρ€Π΅Π½ΠΈΠ΅ покрытия ΠΊΠΎΠ΄Π° тСстами" OFF)

Compilation Options

Of course, we are cool programmers-positives, so we want the maximum level of compile-time diagnostics from the compiler. Not a single mouse will get through.

add_compile_options(
    -Werror

    -Wall
    -Wextra
    -Wpedantic

    -Wcast-align
    -Wcast-qual
    -Wconversion
    -Wctor-dtor-privacy
    -Wenum-compare
    -Wfloat-equal
    -Wnon-virtual-dtor
    -Wold-style-cast
    -Woverloaded-virtual
    -Wredundant-decls
    -Wsign-conversion
    -Wsign-promo
)

We will also disable extensions to fully comply with the C ++ language standard. They are enabled by default in CMake.

if(NOT CMAKE_CXX_EXTENSIONS)
    set(CMAKE_CXX_EXTENSIONS OFF)
endif()

The main goal

Our library consists only of header files, which means that we do not have any output in the form of static or dynamic libraries. On the other hand, in order to use our library from the outside, you need to install it, you need to be able to find it in the system and connect it to your project, and at the same time these very headers, as well as, possibly, some additional properties.

For this purpose, we create an interface library.

add_library(mylib INTERFACE)

We bind the headers to our interface library.

Modern, trendy, youthful use of CMake means that headers, properties, etc. transmitted through a single target. So it's enough to say target_link_libraries(target PRIVATE dependency), and all headers that are associated with the target dependency, will be available for sources belonging to the target target. And you don't need any [target_]include_directories. This will be demonstrated below when parsing CMake script for unit tests.

It is also worth paying attention to the so-called. выраТСния-Π³Π΅Π½Π΅Ρ€Π°Ρ‚ΠΎΡ€Ρ‹: $<...>.

This command associates the headers we need with our interface library, and if our library is connected to any target within the same CMake hierarchy, then the headers from the directory will be associated with it ${CMAKE_CURRENT_SOURCE_DIR}/include, and if our library is installed on the system and connected to another project using the command find_package, then the headers from the directory will be associated with it include relative to the installation directory.

target_include_directories(mylib INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

Set the language standard. Of course, the latest. At the same time, we not only include the standard, but also distribute it to those who will use our library. This is achieved by having the set property have a category INTERFACE (cm. target_compile_features command).

target_compile_features(mylib INTERFACE cxx_std_17)

We get an alias for our library. And for beauty, it will be in a special "namespace". This will be useful when different modules appear in our library, and we go to connect them independently of each other. As in Busta, for example.

add_library(Mylib::mylib ALIAS mylib)

Installation

Installing our headers into the system. Everything is simple here. We say that the folder with all the headers should fall into the directory include regarding the installation location.

install(DIRECTORY include/mylib DESTINATION include)

Next, we tell the build system that we want to be able to call the command in third-party projects find_package(Mylib) and get a target Mylib::mylib.

install(TARGETS mylib EXPORT MylibConfig)
install(EXPORT MylibConfig NAMESPACE Mylib:: DESTINATION share/Mylib/cmake)

The following incantation is to be understood thus. When in a side project we call the command find_package(Mylib 1.2.3 REQUIRED), and at the same time the real version of the installed library will be incompatible with the version 1.2.3, CMake will automatically generate an error. That is, you will not need to keep track of versions manually.

include(CMakePackageConfigHelpers)
write_basic_package_version_file("${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake"
    VERSION
        ${PROJECT_VERSION}
    COMPATIBILITY
        AnyNewerVersion
)
install(FILES "${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake" DESTINATION share/Mylib/cmake)

Tests

If tests are explicitly disabled with corresponding option or our project is a subproject, that is, it is connected to another CMake project using the command add_subdirectory, we do not go further down the hierarchy, and the script, which describes the commands for generating and running tests, simply does not start.

if(NOT MYLIB_TESTING)
    message(STATUS "ВСстированиС ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° Mylib Π²Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΎ")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib Π½Π΅ тСстируСтся Π² Ρ€Π΅ΠΆΠΈΠΌΠ΅ подмодуля")
else()
    add_subdirectory(test)
endif()

Documentation

Documentation will also not be generated in the case of a subproject.

if(NOT IS_SUBPROJECT)
    add_subdirectory(doc)
endif()

Online sandbox

Similarly, the subproject will not have an online sandbox either.

if(NOT IS_SUBPROJECT)
    add_subdirectory(online)
endif()

Script for tests (test/CMakeLists.txt)

The test is

First of all, we find a package with the desired test framework (replace it with your favorite one).

find_package(doctest 2.3.3 REQUIRED)

We create our executable file with tests. Usually, directly to the executable binary, I add only the file in which the function will be main.

add_executable(mylib-unit-tests test_main.cpp)

And the files in which the tests themselves are described, I add later. But it is not necessary to do so.

target_sources(mylib-unit-tests PRIVATE mylib/myfeature.cpp)

We connect dependencies. Please note that we attached only the CMake targets we needed to our binary, and did not call the command target_include_directories. Headers from the test framework and from our Mylib::mylib, as well as build options (in our case, the C++ language standard) crawled along with these targets.

target_link_libraries(mylib-unit-tests
    PRIVATE
        Mylib::mylib
        doctest::doctest
)

Finally, we create a dummy target whose β€œbuild” is equivalent to running tests, and add this target to the default build (this is the responsibility of the attribute ALL). This means that the default build will trigger the tests to run, meaning we will never forget to run them.

add_custom_target(check ALL COMMAND mylib-unit-tests)

ΠŸΠΎΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅

Next, turn on code coverage measurement, if the corresponding option is set. I won't go into the details, because they are more related to the coverage measurement tool than to CMake. It is only important to note that the results will create a goal coverage, with which it is convenient to start measuring coverage.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Π˜Π·ΠΌΠ΅Ρ€Π΅Π½ΠΈΠ΅ покрытия ΠΊΠΎΠ΄Π° тСстами Π²ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΎ")

    target_compile_options(mylib-unit-tests PRIVATE --coverage)
    target_link_libraries(mylib-unit-tests PRIVATE gcov)

    add_custom_target(coverage
        COMMAND
            ${GCOVR_EXECUTABLE}
                --root=${PROJECT_SOURCE_DIR}/include/
                --object-directory=${CMAKE_CURRENT_BINARY_DIR}
        DEPENDS
            check
    )
elseif(MYLIB_COVERAGE AND NOT GCOVR_EXECUTABLE)
    set(MYLIB_COVERAGE OFF)
    message(WARNING "Для Π·Π°ΠΌΠ΅Ρ€ΠΎΠ² покрытия ΠΊΠΎΠ΄Π° тСстами трСбуСтся ΠΏΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΠ° gcovr")
endif()

Documentation script (doc/CMakeLists.txt)

Found Doxygen.

find_package(Doxygen)

Next, we check if the variable with the language is set by the user. If yes, then we don’t touch it, if not, then we take Russian. Then we configure the Doxygen system files. All the necessary variables, including the language, get there during the configuration process (see. ΠΊΠΎΠΌΠ°Π½Π΄Ρƒ configure_file).

Then we create a goal doc, which will start generating documentation. Since generating documentation is not the biggest need in the development process, the target will not be included by default, it will have to be run explicitly.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Russian)
    endif()
    message(STATUS "Doxygen documentation will be generated in ${MYLIB_DOXYGEN_LANGUAGE}")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

Script for online sandbox (online/CMakeLists.txt)

Here we find the third Python and create a target wandbox, which generates a request corresponding to the service API wandbox, and sends it. In response, a link to the finished sandbox comes.

find_program(PYTHON3_EXECUTABLE python3)
if(PYTHON3_EXECUTABLE)
    set(WANDBOX_URL "https://wandbox.org/api/compile.json")

    add_custom_target(wandbox
        COMMAND
            ${PYTHON3_EXECUTABLE} wandbox.py mylib-example.cpp "${PROJECT_SOURCE_DIR}" include |
            curl -H "Content-type: application/json" -d @- ${WANDBOX_URL}
        WORKING_DIRECTORY
            ${CMAKE_CURRENT_SOURCE_DIR}
        DEPENDS
            mylib-unit-tests
    )
else()
    message(WARNING "Для создания ΠΎΠ½Π»Π°ΠΉΠ½-пСсочницы трСбуСтся ΠΈΠ½Ρ‚Π΅Ρ€ΠΏΡ€Π΅Ρ‚Π°Ρ‚ΠΎΡ€ ЯП python 3-ΠΉ вСрсии")
endif()

project outside

Now let's look at how to use it all.

Assembly

Building this project, like any other project on the CMake build system, consists of two stages:

Generation

cmake -S ΠΏΡƒΡ‚ΡŒ/ΠΊ/исходникам -B ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ [ΠΎΠΏΡ†ΠΈΠΈ ...]

If the command above didn't work due to an older version of CMake, try omitting -S:

cmake ΠΏΡƒΡ‚ΡŒ/ΠΊ/исходникам -B ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ [ΠΎΠΏΡ†ΠΈΠΈ ...]

More about options.

Project build

cmake --build ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ [--target target]

More about assembly goals.

Options

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [ΠΏΡ€ΠΎΡ‡ΠΈΠ΅ ΠΎΠΏΡ†ΠΈΠΈ ...]

Includes target coverage, with which you can start measuring code coverage with tests.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [ΠΏΡ€ΠΎΡ‡ΠΈΠ΅ ΠΎΠΏΡ†ΠΈΠΈ ...]

Provides the option to turn off unit test build and target check. As a result, the measurement of code coverage by tests is turned off (see MYLIB_COVERAGE).

Also, testing is automatically disabled if the project is connected to another project as a subproject using the command add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [ΠΏΡ€ΠΎΡ‡ΠΈΠ΅ ΠΎΠΏΡ†ΠΈΠΈ ...]

Switches the language of the documentation that the target generates doc to the given one. For a list of available languages, see Doxygen website.

Russian is enabled by default.

assembly targets

By default

cmake --build path/to/build/directory
cmake --build path/to/build/directory --target all

If the target is not specified (which is equivalent to the target all), collects everything that is possible, and also calls the target check.

mylib-unit-tests

cmake --build path/to/build/directory --target mylib-unit-tests

Compiles unit tests. Enabled by default.

check

cmake --build ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ --target check

Runs built (builds if not already) unit tests. Enabled by default.

Π‘ΠΌ. Ρ‚Π°ΠΊΠΆΠ΅ mylib-unit-tests.

coverage

cmake --build ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ --target coverage

Analyzes running (runs, if not yet) unit tests for code coverage by tests using the program gcovr.

The coverage output will look something like this:

------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: /path/to/cmakecpptemplate/include/
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
TOTAL                                          2       2   100%
------------------------------------------------------------------------------

The target is available only when the option is enabled. MYLIB_COVERAGE.

Π‘ΠΌ. Ρ‚Π°ΠΊΠΆΠ΅ check.

doc

cmake --build ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ --target doc

Starts the generation of documentation for the code using the system Doxygen.

wandbox

cmake --build ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ --target wandbox

The response from the service looks something like this:

{
    "permlink" :    "QElvxuMzHgL9fqci",
    "status" :  "0",
    "url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}

The service is used for this. wandbox. I don’t know how rubber servers they have, but I think that you shouldn’t abuse this feature.

Examples

Build project in debug mode with coverage measurement

cmake -S ΠΏΡƒΡ‚ΡŒ/ΠΊ/исходникам -B ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ --target coverage --parallel 16

Installing a project without pre-building and testing

cmake -S ΠΏΡƒΡ‚ΡŒ/ΠΊ/исходникам -B ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=ΠΏΡƒΡ‚ΡŒ/ΠΊ/установойной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ
cmake --build ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ --target install

Build in release mode by the given compiler

cmake -S ΠΏΡƒΡ‚ΡŒ/ΠΊ/исходникам -B ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=ΠΏΡƒΡ‚ΡŒ/ΠΊ/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ/ΠΊΡƒΠ΄Π°/установлСны/зависимости
cmake --build ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ --parallel 4

Generation of documentation in English

cmake -S ΠΏΡƒΡ‚ΡŒ/ΠΊ/исходникам -B ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ --target doc

Tools

  1. CMake 3.13

    In fact, CMake version 3.13 is only required to run some of the console commands described in this help. From the point of view of the syntax of CMake scripts, version 3.8 is sufficient if the generation is called in other ways.

  2. Testing Library doctest

    Testing can be disabled (see ΠΎΠΏΡ†ΠΈΡŽ MYLIB_TESTING).

  3. Doxygen

    To switch the language in which the documentation will be generated, there is an option MYLIB_DOXYGEN_LANGUAGE.

  4. PL interpreter Python 3

    For automatic generation online sandboxes.

Static Analysis

With the help of CMake and a couple of good tools, you can provide static analysis with minimal fiddling.

Cppcheck

Support for static analysis tool built into CMake Cppcheck.

To do this, use the option CMAKE_CXX_CPPCHECK:

cmake -S ΠΏΡƒΡ‚ΡŒ/ΠΊ/исходникам -B ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-IΠΏΡƒΡ‚ΡŒ/ΠΊ/исходникам/include"

After that, static analysis will be automatically launched every time during compilation and recompilation of sources. Nothing extra needs to be done.

Clang

With a wonderful tool scan-build You can also run static analysis in no time:

scan-build cmake -S ΠΏΡƒΡ‚ΡŒ/ΠΊ/исходникам -B ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build ΠΏΡƒΡ‚ΡŒ/ΠΊ/сборочной/Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ

Here, unlike the case with Cppcheck, it is required to run the build every time through scan-build.

Afterword

CMake is a very powerful and flexible system that allows you to implement functionality for every taste and color. And, although the syntax sometimes leaves much to be desired, the devil is still not as terrible as he is painted. Use the CMake build system for the benefit of society and health.

β†’ Download project template

Source: habr.com

Buy reliable hosting for sites with DDoS protection, VPS VDS servers πŸ”₯ Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster