
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:
- Building;
- Automatic test launch;
- Code coverage measurement;
- Installation;
- Auto-documentation;
- Online sandbox generation;
- Static analysis.
For those already familiar with C++ and CMake, you can simply 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.cppPrimarily, the discussion will focus on how to organize CMake scripts, so they will be covered in detail. Others can view the remaining files directly .
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 ).
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)
Let's consider two options.
The first option is β 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 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)
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()
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 , 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 .
It is also worth noting the so-called .
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 , 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 ).
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. .
add_library(Mylib::mylib ALIAS mylib)
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)
If the tests are explicitly disabled using the or our project is a subproject, meaning it is linked to another CMake project using the command , 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 will also not be generated in the case of a subproject.
if(NOT IS_SUBPROJECT)
add_subdirectory(doc)
endif()
Similarly, there will also be no online sandbox for the subproject.
if(NOT IS_SUBPROJECT)
add_subdirectory(online)
endif()
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)
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 , 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()
.
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 ).
After that, we create a target , 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 ()
Here we find the third Python and create a target , which generates a request corresponding to the Wandbox API service 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.
Building this project, like any other project using the CMake build system, consists of two stages:
cmake -S path/to/source -B path/to/build/directory [options ...]
If the above command fails due to an old version of CMake, try omittingcmake path/to/source -B path/to/build/directory [options ...]
-S:More about the options
.
More about build targets.
Includes the targetIncludes purpose , with which you can start measuring code coverage with tests.
cmake -S ... -B ... -DMYLIB_TESTING=OFF [other options ...]Provides the ability to disable the build of unit tests and targets . As a result, code coverage measurement is turned off (see ).
Testing is also automatically disabled if the project is included in another project as a subproject using the command .
cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [other options ...]Switches the documentation language generated by the target to the specified one. See the list of available languages on .
By default, Russian is enabled.
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), builds everything that can be built and also calls the target .
cmake --build path/to/build/directory --target mylib-unit-testsCompiles unit tests. Enabled by default.
cmake --build path/to/build/directory --target checkRuns the built (builds if not yet) unit tests. Enabled by default.
See also .
cmake --build path/to/build/directory --target coverageAnalyzes the run (runs if not yet) unit tests for code coverage using the program .
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. .
See also .
cmake --build path/to/build/directory --target docStarts generating documentation for the code using the system .
cmake --build path/to/build/directory --target wandboxThe response from the service looks approximately like this:
{
"permlink" : "QElvxuMzHgL9fqci",
"status" : "0",
"url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}This uses the service . I don't know how robust their servers are, but I think it's best not to abuse this capability.
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 16Install 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 installBuild 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 4Generate 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
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.
Testing library
Testing can be disabled (see ).
To switch the language in which the documentation will be generated, there is an option .
Programming Language Interpreter
For automatic generation .
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 .
For this, you need to use the option :
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 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/directoryHere, unlike in the case with Cppcheck, you need to run the build through scan-build.
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.
β
Source: habr.com
