CMake e C++ son irmáns para sempre

CMake e C++ son irmáns para sempre

Durante o desenvolvemento, gústame cambiar compiladores, modos de compilación, versións de dependencia, realizar análises estáticas, medir o rendemento, recoller cobertura, xerar documentación, etc. E realmente encántame CMake porque me permite facer todo o que quero.

Moita xente critica CMake, e moitas veces merecidamente, pero se o miras, non todo está tan mal, e recentemente non está nada mal, e a dirección do desenvolvemento é bastante positiva.

Nesta nota, quero dicirche como simplemente organizar unha biblioteca de cabeceiras en C++ no sistema CMake para obter a seguinte funcionalidade:

  1. Asemblea;
  2. Probas de execución automática;
  3. Medición de cobertura de código;
  4. Instalación;
  5. Documentación automática;
  6. Xeración de sandbox en liña;
  7. Análise estática.

Calquera persoa que xa entenda as vantaxes e C-make pode simplemente Descargar modelo de proxecto e comeza a usalo.


Contido

  1. Proxecto dende dentro
    1. Estrutura do proxecto
    2. Ficheiro principal de CMake (./CMakeLists.txt)
      1. Información do proxecto
      2. Opcións do proxecto
      3. Opcións de compilación
      4. O obxectivo principal
      5. Instalación
      6. Probas
      7. Documentación
      8. Sandbox en liña
    3. Script de proba (test/CMakeLists.txt)
      1. Probas
      2. Cuberta
    4. Script para documentación (doc/CMakeLists.txt)
    5. Script para sandbox en liña (en liña/CMakeLists.txt)
  2. Proxecto fóra
    1. Asemblea
      1. Xeración
      2. Asemblea
    2. Opcións
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Obxectivos de montaxe
      1. Por defecto
      2. probas-unitarias mylib
      3. comprobar
      4. cobertura
      5. doutor
      6. caixa de variñas
    4. Exemplos
  3. Ferramentas
  4. Análise estática
  5. Posterior

Proxecto dende dentro

Estrutura do proxecto

.
├── 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

Falaremos principalmente sobre como organizar os scripts de CMake, polo que se comentarán en detalle. Calquera pode ver o resto dos ficheiros directamente na páxina do proxecto modelo.

Ficheiro principal de CMake (./CMakeLists.txt)

Información do proxecto

Primeiro de todo, cómpre solicitar a versión requirida do sistema CMake. CMake está evolucionando, as sinaturas de comandos e o comportamento en diferentes condicións están cambiando. Para que CMake entenda inmediatamente o que queremos del, necesitamos rexistrar inmediatamente os nosos requisitos para iso.

cmake_minimum_required(VERSION 3.13)

Despois designaremos o noso proxecto, o seu nome, versión, idiomas empregados, etc. (ver. команду project).

Neste caso indicamos a lingua CXX (e isto significa C++) para que CMake non se esforce e busque un compilador de linguaxe C (por defecto, CMake inclúe dúas linguaxes: C e C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Aquí pode comprobar inmediatamente se o noso proxecto está incluído noutro proxecto como subproxecto. Isto axudará moito no futuro.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Opcións do proxecto

Ofreceremos dúas opcións.

A primeira opción é MYLIB_TESTING — para desactivar as probas unitarias. Isto pode ser necesario se estamos seguros de que todo está en orde coas probas, pero só queremos, por exemplo, instalar ou empaquetar o noso proxecto. Ou o noso proxecto inclúese como un subproxecto; neste caso, o usuario do noso proxecto non está interesado en executar as nosas probas. Non probas as dependencias que usas, non si?

option(MYLIB_TESTING "Включить модульное тестирование" ON)

Ademais, faremos unha opción separada MYLIB_COVERAGE para medir a cobertura do código mediante probas, pero requirirá ferramentas adicionais, polo que terá que estar activado de forma explícita.

option(MYLIB_COVERAGE "Включить измерение покрытия кода тестами" OFF)

Opcións de compilación

Por suposto, somos programadores xeniais, polo que queremos o máximo nivel de diagnóstico en tempo de compilación do compilador. Nin un só rato se deslizará.

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
)

Tamén desactivaremos as extensións para cumprir plenamente o estándar da linguaxe C++. Están habilitados por defecto en CMake.

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

O obxectivo principal

A nosa biblioteca está formada só por ficheiros de cabeceira, o que significa que non temos ningún escape en forma de bibliotecas estáticas ou dinámicas. Por outra banda, para poder utilizar externamente a nosa biblioteca é preciso que estea instalada, que sexa detectable no sistema e conectada ao seu proxecto, e ao mesmo tempo estas mesmas cabeceiras, así como posiblemente algunhas adicionais, están unidas a ela propiedades.

Para este fin, creamos unha biblioteca de interfaces.

add_library(mylib INTERFACE)

Enlazamos cabeceiras á nosa biblioteca de interfaces.

O uso moderno, de moda e xuvenil de CMake implica que cabeceiras, propiedades, etc. transmitido a través dun único obxectivo. Así que abonda con dicir target_link_libraries(target PRIVATE dependency), e todas as cabeceiras que están asociadas co destino dependency, estará dispoñible para fontes pertencentes ao destino target. E non precisa ningún [target_]include_directories. Isto demostrarase a continuación na análise Script CMake para probas unitarias.

Tamén paga a pena prestar atención ao chamado. выражения-генераторы: $<...>.

Este comando asocia as cabeceiras que necesitamos coa nosa biblioteca de interfaces, e se a nosa biblioteca está conectada a algún destino dentro da mesma xerarquía de CMake, as cabeceiras do directorio asociaranse a el. ${CMAKE_CURRENT_SOURCE_DIR}/include, e se a nosa biblioteca está instalada no sistema e conectada a outro proxecto mediante o comando find_package, entón as cabeceiras do directorio asociaranse a el include relativo ao directorio de instalación.

target_include_directories(mylib INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

Establecemos un estándar lingüístico. Por suposto, o último. Ao mesmo tempo, non só incluímos o estándar, senón que tamén o estendimos a aqueles que utilicen a nosa biblioteca. Isto conséguese debido ao feito de que a propiedade do conxunto ten unha categoría INTERFACE (Ver. comando target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Imos crear un alias para a nosa biblioteca. Ademais, para a beleza, estará nun "espazo de nomes" especial. Isto será útil cando aparezan diferentes módulos na nosa biblioteca, e imos conectalos de forma independente uns dos outros. Como en Busta, por exemplo.

add_library(Mylib::mylib ALIAS mylib)

Instalación

Instalando as nosas cabeceiras no sistema. Aquí todo é sinxelo. Dicimos que o cartafol con todas as cabeceiras debe ir ao directorio include relativo ao lugar de instalación.

install(DIRECTORY include/mylib DESTINATION include)

A continuación, informamos ao sistema de compilación de que queremos poder chamar ao comando en proxectos de terceiros find_package(Mylib) e conseguir un gol Mylib::mylib.

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

O seguinte feitizo debe entenderse deste xeito. Cando nun proxecto de terceiros chamamos ao comando find_package(Mylib 1.2.3 REQUIRED), e a versión real da biblioteca instalada será incompatible coa versión 1.2.3CMake xerará automaticamente un erro. É dicir, non necesitará rastrexar versións manualmente.

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)

Probas

Se as probas están desactivadas explícitamente usando opción correspondente ou o noso proxecto é un subproxecto, é dicir, está conectado a outro proxecto CMake mediante o comando add_subdirectory, non avanzamos máis na xerarquía e o script, que describe os comandos para xerar e executar probas, simplemente non se executa.

if(NOT MYLIB_TESTING)
    message(STATUS "Тестирование проекта Mylib выключено")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib не тестируется в режиме подмодуля")
else()
    add_subdirectory(test)
endif()

Documentación

Tampouco se xerará documentación no caso dun subproxecto.

if(NOT IS_SUBPROJECT)
    add_subdirectory(doc)
endif()

Sandbox en liña

Así mesmo, o subproxecto tampouco contará cun sandbox en liña.

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

Script de proba (test/CMakeLists.txt)

Probas

Primeiro de todo, atopamos un paquete co marco de proba necesario (substitúeo polo teu favorito).

find_package(doctest 2.3.3 REQUIRED)

Imos crear o noso ficheiro executable con probas. Normalmente engado directamente ao binario executable só o ficheiro que conterá a función main.

add_executable(mylib-unit-tests test_main.cpp)

E engado ficheiros nos que máis adiante se describen as propias probas. Pero non tes que facelo.

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

Conectamos dependencias. Teña en conta que só ligamos os obxectivos CMake que necesitabamos ao noso binario e non chamamos ao comando target_include_directories. Títulos do marco de proba e do noso Mylib::mylib, así como os parámetros de compilación (no noso caso, este é o estándar da linguaxe C++) chegou xunto con estes obxectivos.

target_link_libraries(mylib-unit-tests
    PRIVATE
        Mylib::mylib
        doctest::doctest
)

Finalmente, creamos un obxectivo ficticio, cuxa "construción" é equivalente a realizar probas, e engadimos este destino á compilación predeterminada (o atributo é responsable deste ALL). Isto significa que a compilación predeterminada desencadea a execución das probas, o que significa que nunca esqueceremos executalas.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Cuberta

A continuación, activamos a medición da cobertura do código se se especifica a opción adecuada. Non entrarei en detalles, porque se relacionan máis cunha ferramenta para medir a cobertura que con CMake. Só é importante ter en conta que en función dos resultados crearase un obxectivo coverage, co que é conveniente comezar a medir a cobertura.

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 para documentación (doc/CMakeLists.txt)

Atopouse Doxygen.

find_package(Doxygen)

A continuación, comprobamos se o usuario estableceu a variable de idioma. Se si, entón non o tocamos, se non, entón tomamos ruso. Despois configuramos os ficheiros do sistema Doxygen. Todas as variables necesarias, incluído o idioma, van alí durante o proceso de configuración (ver. команду configure_file).

Despois creamos un obxectivo doc, que comezará a xerar documentación. Dado que a xeración de documentación non é a maior necesidade no proceso de desenvolvemento, o destino non estará activado por defecto; terá que lanzarse de forma explícita.

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 para sandbox en liña (en liña/CMakeLists.txt)

Aquí atopamos o terceiro Python e creamos un destino wandbox, que xera unha solicitude correspondente á API do servizo Caixa de variñas, e despídeo. A resposta vén cunha ligazón ao sandbox rematado.

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

Proxecto fóra

Agora vexamos como usar todo isto.

Asemblea

A construción deste proxecto, como calquera outro proxecto no sistema de compilación CMake, consta de dúas etapas:

Xeración

cmake -S путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Se o comando anterior non funcionou debido a unha versión antiga de CMake, proba omitilo -S:

cmake путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Máis información sobre as opcións.

Construíndo o proxecto

cmake --build путь/к/сборочной/директории [--target target]

Máis información sobre os obxectivos da asemblea.

Opcións

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [прочие опции ...]

Inclúe o obxectivo coverage, co que podes comezar a medir a cobertura do código mediante probas.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [прочие опции ...]

Ofrece a posibilidade de desactivar a compilación e o destino da proba unitaria check. Como resultado, a medición da cobertura de código mediante probas está desactivada (ver. MYLIB_COVERAGE).

As probas tamén se desactivan automaticamente se o proxecto está conectado a outro proxecto como subproxecto mediante o comando add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [прочие опции ...]

Cambia o idioma da documentación que xera o destino doc ao dado. Para obter unha lista dos idiomas dispoñibles, consulte Sitio web do sistema Doxygen.

O ruso está activado por defecto.

Obxectivos de montaxe

Por defecto

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

Se non se especifica o obxectivo (o que equivale ao obxectivo all), recolle todo o que pode e tamén chama ao obxectivo check.

probas-unitarias mylib

cmake --build path/to/build/directory --target mylib-unit-tests

Compila probas unitarias. Activado por defecto.

comprobar

cmake --build путь/к/сборочной/директории --target check

Realiza as probas unitarias recollidas (se non xa). Activado por defecto.

Vexa tamén mylib-unit-tests.

cobertura

cmake --build путь/к/сборочной/директории --target coverage

Analiza probas unitarias en execución (se non xa) para a cobertura de código mediante probas utilizando o programa gcovr.

O escape do revestimento terá un aspecto así:

------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: /path/to/cmakecpptemplate/include/
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
TOTAL                                          2       2   100%
------------------------------------------------------------------------------

O destino só está dispoñible cando a opción está activada MYLIB_COVERAGE.

Vexa tamén check.

doutor

cmake --build путь/к/сборочной/директории --target doc

Inicia a xeración de documentación de código usando o sistema Osíxeno.

caixa de variñas

cmake --build путь/к/сборочной/директории --target wandbox

A resposta do servizo é algo así:

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

O servizo úsase para iso Caixa de variñas. Non sei o flexibles que son os seus servidores, pero creo que non se debe abusar desta oportunidade.

Exemplos

Construír o proxecto en modo depuración coa medición de cobertura

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build путь/к/сборочной/директории --target coverage --parallel 16

Instalación dun proxecto sen montaxe e probas previas

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=путь/к/установойной/директории
cmake --build путь/к/сборочной/директории --target install

Construír en modo de lanzamento cun compilador determinado

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=путь/к/директории/куда/установлены/зависимости
cmake --build путь/к/сборочной/директории --parallel 4

Xeración de documentación en inglés

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build путь/к/сборочной/директории --target doc

Ferramentas

  1. CMake 3.13

    De feito, a versión 3.13 de CMake só é necesaria para executar algúns dos comandos da consola descritos nesta axuda. Desde o punto de vista da sintaxe dos scripts CMake, a versión 3.8 é suficiente se a xeración se chama doutro xeito.

  2. Biblioteca de probas doctest

    A proba pódese desactivar (ver опцию MYLIB_TESTING).

  3. Osíxeno

    Para cambiar o idioma no que se xerará a documentación, ofrécese unha opción MYLIB_DOXYGEN_LANGUAGE.

  4. Intérprete lingüístico python 3

    Para xeración automática caixas de area en liña.

Análise estática

Con CMake e un par de boas ferramentas, pode proporcionar análise estática cun esforzo mínimo.

Cppcheck

CMake ten soporte integrado para unha ferramenta de análise estática Cppcheck.

Para iso cómpre utilizar a opción CMAKE_CXX_CPPCHECK:

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Iпуть/к/исходникам/include"

Despois diso, a análise estática lanzarase automaticamente cada vez que se compile e recompile a fonte. Non hai que facer nada adicional.

berrar

Coa axuda dunha ferramenta marabillosa scan-build Tamén pode executar análise estática en pouco tempo:

scan-build cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build путь/к/сборочной/директории

Aquí, a diferenza do caso de Cppcheck, cómpre executar a compilación cada vez scan-build.

Posterior

CMake é un sistema moi potente e flexible que che permite implementar funcionalidades para todos os gustos e cor. E, aínda que a sintaxe ás veces deixa moito que desexar, o demo aínda non é tan terrible como está pintado. Use o sistema de construción CMake para o beneficio da sociedade e da saúde.

Descargar modelo de proxecto

Fonte: www.habr.com

Engadir un comentario