πŸ“œ ⬆️ ⬇️

CMake and C ++ - brothers forever

Friendship forever


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:


  1. Assembly;
  2. Autostart tests;
  3. Measurement of code coverage;
  4. Installation;
  5. Auto documenting
  6. Online sandbox generation;
  7. Static Analysis

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


Content


  1. Project from the inside out
    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. Testing
      2. Coating
    4. Script for documentation (doc / CMakeLists.txt)
    5. Script for an 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 goals
      1. Default
      2. mylib-unit-tests
      3. check
      4. coverage
      5. doc
      6. wandbox
    4. Examples
  3. Instruments
  4. Static analysis
  5. Afterword


Project from the inside out



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 

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 .



Main CMake file (./CMakeLists.txt)



Project Information


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) 


Project Options


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) 


Compilation options


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() 


the main goal


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) 


Installation


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) 


Tests


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


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 also not have online sandboxes.


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


Script for tests (test / CMakeLists.txt)



Testing


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) 


Coating


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() 


Script for documentation (doc / CMakeLists.txt)


Found Doxygen .


 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 () 


Script for an online sandbox (online / CMakeLists.txt)


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() 


Project outside


Now consider how to use it all.



Assembly


The assembly of this project, like any other project on the CMake assembly system, consists of two stages:



Generation


 cmake -S // -B /// [ ...] 

If the command above did not work due to the old version of CMake, try omitting -S :
 cmake // -B /// [ ...] 

More about options .



Project assembly


 cmake --build /// [--target target] 

Read more about assembly goals .



Options



MYLIB_COVERAGE


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

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



MYLIB_TESTING


 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 .



MYLIB_DOXYGEN_LANGUAGE


 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.



Assembly goals



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 all goal), collects everything that is possible and also calls the check target.



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 collected (collects, if not yet) unit tests. Enabled by default.


See also mylib-unit-tests .



coverage


 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 .



doc


 cmake --build /// --target doc 

Starts the generation of code documentation using the Doxygen system.



wandbox


 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.



Examples


Project assembly in debug mode with coverage measurement


 cmake -S // -B /// -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON cmake --build /// --target coverage --parallel 16 

Project installation without pre-assembly and testing


 cmake -S // -B /// -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=/// cmake --build /// --target install 

Build in release mode by the specified compiler


 cmake -S // -B /// -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=///// cmake --build /// --parallel 4 

English documentation generation


 cmake -S // -B /// -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English cmake --build /// --target doc 


Instruments


  1. 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.


  2. Doctest testing library


    Testing can be disabled (see MYLIB_TESTING ).


  3. Doxygen


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


  4. Python 3 Interpreter


    To automatically generate online sandboxes .




Static analysis


With CMake and a couple of good tools, you can provide static analysis with minimal body movements.


Cppcheck


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.


Clang


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 .



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 not so terrible as he is painted. Use the CMake build system for the benefit of society and health.




β†’ Download project template


')

Source: https://habr.com/ru/post/461817/


All Articles