
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:
- Assembly;
- Autorun tests;
- Code coverage measurement;
- installation;
- Autodocumentation;
- Online sandbox generation;
- Static analysis.
Who already understands the pros and si-make can just and start using it.
Content
.
βββ 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.cppThis 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. .
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. ).
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)
Let's consider two options.
The first option is - 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 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 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()
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 , 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 .
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 , 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(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. .
add_library(Mylib::mylib ALIAS mylib)
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)
If tests are explicitly disabled with or our project is a subproject, that is, it is connected to another CMake project using the command , 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 will also not be generated in the case of a subproject.
if(NOT IS_SUBPROJECT)
add_subdirectory(doc)
endif()
Similarly, the subproject will not have an online sandbox either.
if(NOT IS_SUBPROJECT)
add_subdirectory(online)
endif()
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 , 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 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. ).
Then we create a goal , 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 ()
Here we find the third Python and create a target , which generates a request corresponding to the 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 let's look at how to use it all.
Building this project, like any other project on the CMake build system, consists of two stages:
cmake -S ΠΏΡΡΡ/ΠΊ/ΠΈΡΡ
ΠΎΠ΄Π½ΠΈΠΊΠ°ΠΌ -B ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ [ΠΎΠΏΡΠΈΠΈ ...]If the command above didn't work due to an older version of CMake, try omitting
-S:cmake ΠΏΡΡΡ/ΠΊ/ΠΈΡΡ ΠΎΠ΄Π½ΠΈΠΊΠ°ΠΌ -B ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ [ΠΎΠΏΡΠΈΠΈ ...]
.
cmake --build ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ [--target target].
cmake -S ... -B ... -DMYLIB_COVERAGE=ON [ΠΏΡΠΎΡΠΈΠ΅ ΠΎΠΏΡΠΈΠΈ ...]Includes target , with which you can start measuring code coverage with tests.
cmake -S ... -B ... -DMYLIB_TESTING=OFF [ΠΏΡΠΎΡΠΈΠ΅ ΠΎΠΏΡΠΈΠΈ ...]Provides the option to turn off unit test build and target . As a result, the measurement of code coverage by tests is turned off (see ).
Also, testing is automatically disabled if the project is connected to another project as a subproject using the command .
cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [ΠΏΡΠΎΡΠΈΠ΅ ΠΎΠΏΡΠΈΠΈ ...]Switches the language of the documentation that the target generates to the given one. For a list of available languages, see .
Russian is enabled by default.
cmake --build path/to/build/directory
cmake --build path/to/build/directory --target allIf the target is not specified (which is equivalent to the target all), collects everything that is possible, and also calls the target .
cmake --build path/to/build/directory --target mylib-unit-testsCompiles unit tests. Enabled by default.
cmake --build ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ --target checkRuns built (builds if not already) unit tests. Enabled by default.
Π‘ΠΌ. ΡΠ°ΠΊΠΆΠ΅ .
cmake --build ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ --target coverageAnalyzes running (runs, if not yet) unit tests for code coverage by tests using the program .
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. .
Π‘ΠΌ. ΡΠ°ΠΊΠΆΠ΅ .
cmake --build ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ --target docStarts the generation of documentation for the code using the system .
cmake --build ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ --target wandboxThe response from the service looks something like this:
{
"permlink" : "QElvxuMzHgL9fqci",
"status" : "0",
"url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}The service is used for this. . I donβt know how rubber servers they have, but I think that you shouldnβt abuse this feature.
Build project in debug mode with coverage measurement
cmake -S ΠΏΡΡΡ/ΠΊ/ΠΈΡΡ
ΠΎΠ΄Π½ΠΈΠΊΠ°ΠΌ -B ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ --target coverage --parallel 16Installing a project without pre-building and testing
cmake -S ΠΏΡΡΡ/ΠΊ/ΠΈΡΡ
ΠΎΠ΄Π½ΠΈΠΊΠ°ΠΌ -B ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=ΠΏΡΡΡ/ΠΊ/ΡΡΡΠ°Π½ΠΎΠ²ΠΎΠΉΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ
cmake --build ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ --target installBuild 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 4Generation of documentation in English
cmake -S ΠΏΡΡΡ/ΠΊ/ΠΈΡΡ
ΠΎΠ΄Π½ΠΈΠΊΠ°ΠΌ -B ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build ΠΏΡΡΡ/ΠΊ/ΡΠ±ΠΎΡΠΎΡΠ½ΠΎΠΉ/Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ --target doc
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.
Testing Library
Testing can be disabled (see ).
To switch the language in which the documentation will be generated, there is an option .
PL interpreter
For automatic generation .
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 .
To do this, use the option :
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 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.
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.
β
Source: habr.com
