During the development process, I like to change compilers, build modes, dependency versions, perform static analysis, measure performance, collect coverage, generate documentation, etc. And I really love CMake, because it allows me to do everything that I want.
Many criticize CMake, and often deservedly, but if you look, not everything is so bad, and recently itβs very good , and the direction of development is quite positive.
In this article I want to tell how simple it is to organize a C ++ header library in the CMake system to get the following functionality:
Whoever already understands the pros and s-make can just download the project template and start using it.
. βββ 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
We will mainly talk about how to organize CMake scripts, so they will be analyzed in detail. Everyone can see the rest of the files directly on the project-template page .
First of all, you need to request the right version of the CMake system. CMake is developing, team signatures, behavior in different conditions are changing. In order for CMake to immediately understand what we want from him, we need to immediately fix our requirements for him.
cmake_minimum_required(VERSION 3.13)
Then we designate our project, its name, version, languages ββused, etc. (see project
).
In this case, we specify the CXX
language (which means C ++) so that CMake does not strain and does not look for the C language 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 greatly help in the future.
get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)
We provide two options.
The first option - MYLIB_TESTING
- to turn off 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 do not test the dependencies that you use?
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)
Of course, we are cool plus programmers, so we want the maximum level of compilation time diagnostics from the compiler. Not a single mouse will slip 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 included in CMake.
if(NOT CMAKE_CXX_EXTENSIONS) set(CMAKE_CXX_EXTENSIONS OFF) endif()
Our library consists only of header files, which means that we do not have any exhaust in the form of static or dynamic libraries. On the other hand, to use our library 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 same headers, as well as, perhaps, some additional ones properties.
For this purpose, we create an interface library.
add_library(mylib INTERFACE)
Bind the headers to our front-end library.
The modern, fashionable, youthful use of CMake implies that headers, properties, etc. transmitted through one single purpose. Thus, suffice it to say target_link_libraries(target PRIVATE dependency)
, and all the headers that are associated with the dependency
target will be available for sources belonging to the target target
. And no [target_]include_directories
is required. This will be demonstrated below when parsing the CMake script for unit tests .
It is also worth paying attention to the so-called -: $<...>
.
This command associates the headers we need with our front-end library, and if our library is connected to any target within the same CMake hierarchy, then headers from the directory ${CMAKE_CURRENT_SOURCE_DIR}/include
will be associated with it, and if ours Since the library is installed on the system and connected to another project using the find_package
, the headers from the include
directory relative to the installation directory will be associated with it.
target_include_directories(mylib INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include> )
Set the language standard. Of course, the very last. At the same time, we not only include the standard, but also distribute it to those who will use our library. This is achieved due to the fact that the set property has the INTERFACE
category (see the target_compile_features command ).
target_compile_features(mylib INTERFACE cxx_std_17)
We create an alias for our library. Moreover, for beauty, he will be in a special "namespace". This will be useful when different modules appear in our library, and we go to connect them independently from each other. Like in Boost, for example .
add_library(Mylib::mylib ALIAS mylib)
Installing our headers in the system. Everything is simple here. We say that the folder with all the headers should be in the include
directory 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 find_package(Mylib)
in third-party projects and get the target Mylib::mylib
.
install(TARGETS mylib EXPORT MylibConfig) install(EXPORT MylibConfig NAMESPACE Mylib:: DESTINATION share/Mylib/cmake)
The next spell should be understood as follows. When we call find_package(Mylib 1.2.3 REQUIRED)
in a third-party project, and in this case the real version of the installed library will be incompatible with version 1.2.3
, CMake will automatically generate an error. That is, you will not need to follow the 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)
If tests are turned off explicitly using the appropriate option, or if our project is a subproject, that is, connected to another CMake project using the add_subdirectory
, we will not go further in the hierarchy, and the script that describes the commands for generating and running the tests simply does not start .
if(NOT MYLIB_TESTING) message(STATUS " Mylib ") elseif(IS_SUBPROJECT) message(STATUS "Mylib ") else() add_subdirectory(test) endif()
Documentation will also not be generated in the case of a subproject.
if(NOT IS_SUBPROJECT) add_subdirectory(doc) endif()
Similarly, the subproject will also not have online sandboxes.
if(NOT IS_SUBPROJECT) add_subdirectory(online) endif()
First of all, we find the package with the desired test framework (replace it with your favorite).
find_package(doctest 2.3.3 REQUIRED)
We create our executable file with tests. Usually directly in the executable binary I add only the file in which the main
function will be.
add_executable(mylib-unit-tests test_main.cpp)
And the files that describe the tests themselves are added later. But this is not necessary.
target_sources(mylib-unit-tests PRIVATE mylib/myfeature.cpp)
We connect dependencies. Please note that we only target_include_directories
CMake targets we needed to our binary, and did not call the target_include_directories
command. The headers from the test framework and from our Mylib::mylib
, as well as the build parameters (in our case, this is the C ++ language standard) crawled along with these goals.
target_link_libraries(mylib-unit-tests PRIVATE Mylib::mylib doctest::doctest )
Finally, create a fictitious target, the "assembly" of which is equivalent to running tests, and add this target to the default assembly (the ALL
attribute is responsible for this). This means that the assembly by default initiates the launch of tests, that is, we will never forget to run them.
add_custom_target(check ALL COMMAND mylib-unit-tests)
Next, we enable code coverage measurement, if the corresponding option is specified. I wonβt go into details, because they relate more to the tool for measuring coatings than to CMake. It is only important to note that, based on the results, a coverage
goal will be created, 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()
find_package(Doxygen)
Next, we check whether the user has set the language variable. If yes, then do not touch, if not, then take Russian. Then configure the Doxygen system files. All the necessary variables, including the language, get there during the configuration process (see configure_file
).
Then we create the doc
target, which will start the generation of documentation. Since the generation of documentation is not the greatest need in the development process, by default the goal will not be enabled, it will have to be started 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 ()
Here we find the third Python and create a wandbox
target that generates a request that matches the Wandbox service API 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()
Now consider how to use it all.
The assembly of this project, like any other project on the CMake assembly system, consists of two stages:
cmake -S // -B /// [ ...]
If the command above did not work due to the old version of CMake, try omitting-S
:cmake // -B /// [ ...]
cmake --build /// [--target target]
Read more about assembly goals .
cmake -S ... -B ... -DMYLIB_COVERAGE=ON [ ...]
Includes a coverage
target, with which you can start measuring coverage of code with tests.
cmake -S ... -B ... -DMYLIB_TESTING=OFF [ ...]
Provides the ability to turn off the unit test assembly and the check
target. As a result, the measurement of code coverage by tests is MYLIB_COVERAGE
off (see MYLIB_COVERAGE
).
Also, testing is automatically disabled if the project is connected to another project as a subproject using the add_subdirectory
.
cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [ ...]
Switches the documentation language that the doc
target generates to the specified one. For a list of available languages, see the Doxygen website .
By default, Russian is enabled.
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 all
goal), collects everything that is possible and also calls the check
target.
cmake --build path/to/build/directory --target mylib-unit-tests
Compiles unit tests. Enabled by default.
cmake --build /// --target check
Runs collected (collects, if not yet) unit tests. Enabled by default.
See also mylib-unit-tests
.
cmake --build /// --target coverage
Analyzes running (runs, if not yet) unit tests for covering the code with tests using the gcovr program.
Coating exhaust 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 only available when MYLIB_COVERAGE
.
See also check
.
cmake --build /// --target doc
Starts the generation of code documentation using the Doxygen system.
cmake --build /// --target wandbox
The response from the service looks something like this:
{ "permlink" : "QElvxuMzHgL9fqci", "status" : "0", "url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci" }
To do this, use the Wandbox service. I donβt know how rubber servers they have, but I think itβs not worth it to abuse this feature.
cmake -S // -B /// -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON cmake --build /// --target coverage --parallel 16
cmake -S // -B /// -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=/// cmake --build /// --target install
cmake -S // -B /// -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=///// cmake --build /// --parallel 4
cmake -S // -B /// -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English cmake --build /// --target doc
CMake 3.13
In fact, CMake 3.13 is only required to run some of the console commands described in this help. From the syntax point of view of CMake scripts, version 3.8 is sufficient if generation is called in other ways.
Testing can be disabled (see MYLIB_TESTING
).
To switch the language in which the documentation will be generated, the MYLIB_DOXYGEN_LANGUAGE
option is MYLIB_DOXYGEN_LANGUAGE
.
Python 3 Interpreter
To automatically generate online sandboxes .
With CMake and a couple of good tools, you can provide static analysis with minimal body movements.
CMake has built-in support for the Cppcheck static analysis tool .
To do this, use the CMAKE_CXX_CPPCHECK
option:
cmake -S // -B /// -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-I///include"
After that, the static analysis will be automatically started every time during compilation and recompilation of the sources. You do not need to do anything extra.
Using the wonderful scan-build
tool, you can also run static analysis in two counts:
scan-build cmake -S // -B /// -DCMAKE_BUILD_TYPE=Debug scan-build cmake --build ///
Here, unlike the case with Cppcheck, you need to run the assembly every time through scan-build
.
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 not so terrible as he is painted. Use the CMake build system for the benefit of society and health.
Source: https://habr.com/ru/post/461817/
All Articles