CMake and C++ are brothers forever

CMake and C++ are brothers forever

During development, I enjoy switching compilers, build modes, dependency versions, performing static analysis, measuring performance, collecting coverage, generating documentation, etc. I particularly love CMake because it allows me to do everything I want.

Many criticize CMake, often with good reason, but if you dig deeper, it's not all that bad and lately, it's actually quite good., and the direction of development is quite positive.

In this note, I want to explain how relatively easy it is to organize a header library in C++ using CMake to achieve the following functionalities:

  1. Building;
  2. Automatic test launch;
  3. Code coverage measurement;
  4. Installation;
  5. Auto-documentation;
  6. Online sandbox generation;
  7. Static analysis.

For those already familiar with C++ and CMake, you can simply download the project template and start using it.


Content

  1. Project structure
    1. Project Structure
    2. Main CMake file (./CMakeLists.txt)
      1. Project Information
      2. Project Options
      3. Compilation Options
      4. Main Target
      5. Installation
      6. Tests
      7. Documentation
      8. Online Sandbox
    3. Test script (test/CMakeLists.txt)
      1. Testing
      2. Coverage
    4. Documentation script (doc/CMakeLists.txt)
    5. Online sandbox script (online/CMakeLists.txt)
  2. Project layout
    1. Building
      1. Generation
      2. Building
    2. Options
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Build Targets
      1. the net/http
      2. mylib-unit-tests
      3. check
      4. coverage
      5. doc
      6. wandbox
    4. Examples
  3. Tools
  4. Static analysis
  5. Afterword

Project structure

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

Primarily, the discussion will focus on how to organize CMake scripts, so they will be covered in detail. Others can view the remaining files directly on the template project's page..

Main CMake file (./CMakeLists.txt)

Project Information

First, we need to request the required version of CMake. CMake evolves, command signatures change, and behavior in different conditions varies. To ensure that CMake immediately understands our requirements, we should define them upfront.

cmake_minimum_required(VERSION 3.13)

Next, let's specify our project, its name, version, used languages, and more (see the project command).).

Here, we specify the language CXX (which means C++), so CMake doesn't get confused and look for a C compiler (by default, CMake includes two languages: C and C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Here you can also check whether our project is included in another project as a subproject. This will be very helpful later on.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Project Options

Let's consider two options.

The first option is MYLIB_TESTING β€” to disable unit testing. This may be necessary if we are confident that the tests are fine and we want to, for example, just install or package our project. Or if our project is included as a subproject β€” in that case, the user of our project may not be interested in running our tests. You don't test the dependencies you use, do you?

option(MYLIB_TESTING "Enable unit testing" ON)

Additionally, we will create a separate option MYLIB_COVERAGE for measuring code coverage with tests, but it will require additional tools, so it needs to be explicitly enabled.

option(MYLIB_COVERAGE "Enable code coverage measurement with tests" OFF)

Compilation Options

Of course, we are cool C++ programmers, so we want the compiler to provide the maximum level of compile-time diagnostics. Not a single mouse will pass 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. By default, they are enabled in CMake.

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

Main Target

Our library consists only of header files, and therefore we do not have any output in the form of static or dynamic libraries. On the other hand, in order to use our library externally, it needs to be installed, so that it can be discovered in the system and linked to your project, along with the necessary headers, and possibly some additional properties.

For this purpose, we create an interface library.

add_library(mylib INTERFACE)

We attach the headers to our interface library.

Modern, trendy, youth-oriented use of CMake implies that headers, properties, etc., are passed through a single target. Thus, it is enough to say target_link_libraries(target PRIVATE dependency), and all headers associated with the target dependency, will be available for the source files belonging to the target target. And no [target_]include_directoriesThis will be demonstrated below when analyzing the CMake script for the unit tests.

It is also worth noting the so-called generator expressions: $.

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

target_include_directories(mylib INTERFACE
    $
    $
)

Let's set the language standard. Of course, the latest one. In this case, we are not just including the standard but also extending it to those who will use our library. This is achieved by the fact that the established property has the category INTERFACE (see the target_compile_features command).

target_compile_features(mylib INTERFACE cxx_std_17)

We create an alias for our library. For aesthetics, it will be in a special 'namespace'. This will be useful when our library has different modules, and we want to connect them independently. As in Boost, for example.

add_library(Mylib::mylib ALIAS mylib)

Installation

Installing our headers into the system. This part is straightforward. We indicate that the folder containing all headers should go into the directory include relative to the installation location.

install(DIRECTORY include/mylib DESTINATION include)

Next, we inform the build system that we want to be able to call the command find_package(Mylib) and obtain the target Mylib::mylib.

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

The next command should be understood as follows. When we call the command in an external project find_package(Mylib 1.2.3 REQUIRED), and if the actual installed library version is incompatible with the version 1.2.3, CMake will automatically generate an error. Thus, there's no need to manually track the versions.

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 the tests are explicitly disabled using the corresponding option or our project is a subproject, meaning it is linked to another CMake project using the command add_subdirectory, we do not go deeper in the hierarchy, and the script that describes the commands for generating and running tests simply does not execute.

if(NOT MYLIB_TESTING)
    message(STATUS "Project Mylib testing is turned off")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib is not tested in submodule mode")
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, there will also be no online sandbox for the subproject.

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

Test script (test/CMakeLists.txt)

Testing

First, we find the package with the required testing framework (replace with your favorite).

find_package(doctest 2.3.3 REQUIRED)

We create our executable file with tests. Typically, I directly add only the file that will contain the function to the executable binary. main.

add_executable(mylib-unit-tests test_main.cpp)

But I add the files that describe the tests later. However, this is not mandatory.

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

We connect dependencies. Note that we have only linked the necessary CMake targets to our binary and did not call the command target_include_directories. Header files from the testing framework and from our Mylib::mylib, as well as build parameters (in our case, this is the C++ language standard), have been included 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 we add this target to the default build (this is controlled by the attribute ALL). This means that the default build initiates the running of tests, so we will never forget to run them.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Coverage

Next, we enable code coverage measurement if the corresponding option is set. I won’t go into details because they pertain more to the tool for coverage measurement than to CMake. It’s just important to note that based on the results, a target will be created coverage, which makes it convenient to run the coverage measurement.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Code coverage measurement is enabled")

    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 "The gcovr program is required for code coverage measurements")
endif()

Documentation script (doc/CMakeLists.txt)

Found Doxygen.

find_package(Doxygen)

Next, we check if the user-defined variable for the language is set. If yes, we leave it as is; if not, we take Russian. Then we configure the Doxygen system files. All necessary variables, including the language, are included during the configuration process (see the configure_file command).

After that, we create a target doc, which will trigger the documentation generation. Since generating documentation is not the highest priority in the development process, the target will not be included by default and will have to be executed 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 ()

Online sandbox script (online/CMakeLists.txt)

Here we find the third Python and create a target wandbox, which generates a request corresponding to the Wandbox API service , and sends it. In response, we get a link to the ready sandbox.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 "An interpreter for Python version 3 is required to create an online sandbox") endif()

Now let’s consider how to use all of this.

Project layout

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

Building

cmake -S path/to/source -B path/to/build/directory [options ...]

Generation

If the above command fails due to an old version of CMake, try omitting

cmake path/to/source -B path/to/build/directory [options ...] -S:

More about the options

Building the project.

cmake --build path/to/build/directory [--target target]

More about build targets

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [other options ...].

Options

MYLIB_COVERAGE

Includes the target

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

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [other options ...]

Provides the ability to disable the build of unit tests and targets check. As a result, code coverage measurement is turned off (see MYLIB_COVERAGE).

Testing is also automatically disabled if the project is included in another project as a subproject using the command add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [other options ...]

Switches the documentation language generated by the target doc to the specified one. See the list of available languages on the Doxygen system website..

By default, Russian is enabled.

Build Targets

the net/http

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), builds everything that can be built 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 path/to/build/directory --target check

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

See also mylib-unit-tests.

coverage

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

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

The coverage output will look approximately 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.

See also check.

doc

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

Starts generating documentation for the code using the system Doxygen.

wandbox

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

The response from the service looks approximately like this:

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

This uses the service , and sends it. In response, we get a link to the ready sandbox.. I don't know how robust their servers are, but I think it's best not to abuse this capability.

Examples

Building the project in debug mode with coverage measurement

cmake -S path/to/source -B path/to/build/directory -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build path/to/build/directory --target coverage --parallel 16

Install the project without prior build and testing

cmake -S path/to/source -B path/to/build/directory -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=path/to/install/directory
cmake --build path/to/build/directory --target install

Build in release mode with the specified compiler

cmake -S path/to/source -B path/to/build/directory -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=path/to/directory/where/dependencies/are/installed
cmake --build path/to/build/directory --parallel 4

Generate documentation in English

cmake -S path/to/source -B path/to/build/directory -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build path/to/build/directory --target doc

Tools

  1. CMake 3.13

    In fact, CMake version 3.13 is only required to run certain console commands described in this documentation. For the syntax of CMake scripts, version 3.8 is sufficient if the generation is invoked in other ways.

  2. Testing library doctest

    Testing can be disabled (see the MYLIB_TESTING option).

  3. Doxygen

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

  4. Programming Language Interpreter Python 3

    For automatic generation online sandbox.

Static analysis

With CMake and a couple of good tools, you can ensure static analysis with minimal effort.

Cppcheck

CMake has built-in support for a static analysis tool Cppcheck.

For this, you need to use the option CMAKE_CXX_CPPCHECK:

cmake -S path/to/source -B path/to/build/directory -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Ipath/to/source/include"

After this, static analysis will automatically start every time during the compilation and recompilation of sources. No additional actions are needed.

Clang

With the help of a wonderful tool scan-build you can also run static analysis in no time:

scan-build cmake -S path/to/source -B path/to/build/directory -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build path/to/build/directory

Here, unlike in the case with Cppcheck, you need to run the build through scan-build.

Afterword

CMake is a very powerful and flexible system, allowing for functionality of all sorts. And, although the syntax sometimes leaves much to be desired, it's not as bad as it's depicted. Use the CMake build system for the good of society and for your own benefit.

β†’ Download the project template

Source: habr.com

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