CMake y C++ son hermanos para siempre

CMake y C++ son hermanos para siempre

Durante el desarrollo, me gusta cambiar compiladores, modos de compilación, versiones de dependencia, realizar análisis estáticos, medir el rendimiento, recopilar cobertura, generar documentación, etc. Y realmente amo CMake porque me permite hacer lo que quiera.

Mucha gente regaña a CMake, y muchas veces con razón, pero si miras, no está tan mal, pero últimamente No está mal, y la dirección del desarrollo es bastante positiva.

En esta nota, quiero contarles lo fácil que es organizar una biblioteca de encabezados C++ en un sistema CMake para obtener la siguiente funcionalidad:

  1. asamblea;
  2. Pruebas de ejecución automática;
  3. Medición de cobertura de código;
  4. instalación;
  5. Autodocumentación;
  6. Generación de sandbox en línea;
  7. Análisis estático.

Quien ya entiende los pros y si-make puede simplemente descargar plantilla de proyecto y empieza a usarlo.


contenido

  1. Proyecto desde dentro
    1. Estructura del proyecto
    2. Archivo principal de CMake (./CMakeLists.txt)
      1. Información del proyecto
      2. Opciones de proyecto
      3. Opciones de compilación
      4. El objetivo principal
      5. Instalación
      6. Pruebas
      7. Документация
      8. Caja de arena en línea
    3. Script para pruebas (test/CMakeLists.txt)
      1. pruebas
      2. Покрытие
    4. Script de documentación (doc/CMakeLists.txt)
    5. Script para sandbox en línea (en línea/CMakeLists.txt)
  2. proyecto afuera
    1. asamblea
      1. Generación
      2. asamblea
    2. Opciones
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. objetivos de montaje
      1. Por defecto
      2. pruebas-unitarias-mylib
      3. comprobar
      4. cobertura
      5. doc
      6. caja de varitas
    4. Примеры
  3. Instrumentos
  4. Análisis estático
  5. Epílogo

Proyecto desde dentro

Estructura del proyecto

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

Esto se centrará principalmente en cómo organizar los scripts de CMake, por lo que se analizarán en detalle. El resto de archivos puede ser visto directamente por cualquier persona. en la página del proyecto de plantilla.

Archivo principal de CMake (./CMakeLists.txt)

Información del proyecto

En primer lugar, debe solicitar la versión deseada del sistema CMake. CMake se desarrolla, las firmas de comandos cambian y el comportamiento bajo diferentes condiciones. Para que CMake comprenda de inmediato lo que queremos de él, debemos corregir de inmediato nuestros requisitos.

cmake_minimum_required(VERSION 3.13)

Luego denotamos nuestro proyecto, su nombre, versión, idiomas utilizados, etc. команду project).

En este caso, especifique el idioma. CXX (que significa C++) para que CMake no se moleste en buscar un compilador de C (de forma predeterminada, se incluyen dos lenguajes en CMake: C y C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Aquí puede comprobar inmediatamente si nuestro proyecto está incluido en otro proyecto como subproyecto. Esto ayudará mucho en el futuro.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Opciones de proyecto

Consideremos dos opciones.

La primera opción es MYLIB_TESTING - para deshabilitar las pruebas unitarias. Esto puede ser necesario si estamos seguros de que todo está en orden con las pruebas y queremos, por ejemplo, solo instalar o empaquetar nuestro proyecto. O nuestro proyecto se incluye como un subproyecto; en este caso, el usuario de nuestro proyecto no está interesado en ejecutar nuestras pruebas. No pruebas las dependencias que usas, ¿verdad?

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

Además, haremos una opción separada. MYLIB_COVERAGE para medir la cobertura del código con pruebas, pero requerirá herramientas adicionales, por lo que deberá habilitarlo explícitamente.

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

Opciones de compilación

Por supuesto, somos buenos programadores, por lo que queremos el nivel máximo de diagnóstico en tiempo de compilación del compilador. Ni un solo ratón pasará.

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
)

También desactivaremos las extensiones para cumplir plenamente con el estándar del lenguaje C++. Están habilitados de forma predeterminada en CMake.

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

El objetivo principal

Nuestra biblioteca consta únicamente de archivos de encabezado, lo que significa que no tenemos ningún resultado en forma de bibliotecas estáticas o dinámicas. Por otro lado, para poder utilizar nuestra biblioteca desde el exterior, necesita instalarla, debe poder encontrarla en el sistema y conectarla a su proyecto, y al mismo tiempo estos mismos encabezados también como, posiblemente, algunas propiedades adicionales.

Para ello, creamos una biblioteca de interfaz.

add_library(mylib INTERFACE)

Vinculamos los encabezados a nuestra biblioteca de interfaz.

El uso moderno, moderno y juvenil de CMake significa que los encabezados, propiedades, etc. transmitido a través de un único objetivo. Así que basta con decir target_link_libraries(target PRIVATE dependency)y todos los encabezados asociados con el objetivo dependency, estará disponible para fuentes pertenecientes al objetivo target. Y no necesitas ninguno [target_]include_directories. Esto se demostrará a continuación al analizar Script CMake para pruebas unitarias.

También vale la pena prestar atención a los llamados. выражения-генераторы: $<...>.

Este comando asocia los encabezados que necesitamos con nuestra biblioteca de interfaz, y si nuestra biblioteca está conectada a cualquier objetivo dentro de la misma jerarquía de CMake, entonces los encabezados del directorio se asociarán con él. ${CMAKE_CURRENT_SOURCE_DIR}/include, y si nuestra biblioteca está instalada en el sistema y conectada a otro proyecto usando el comando find_package, entonces los encabezados del directorio se asociarán con él include relativo al directorio de instalación.

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

Establecer el estándar del idioma. Por supuesto, lo último. Al mismo tiempo, no solo incluimos el estándar, sino que también lo distribuimos a quienes utilizarán nuestra biblioteca. Esto se logra haciendo que la propiedad establecida tenga una categoría INTERFACE (Ver. comando target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Obtenemos un alias para nuestra biblioteca. Y para la belleza, estará en un "espacio de nombres" especial. Esto nos será útil cuando en nuestra biblioteca aparezcan diferentes módulos y vayamos a conectarlos de forma independiente entre sí. Como en Busta, por ejemplo..

add_library(Mylib::mylib ALIAS mylib)

Instalación

Instalación de nuestros encabezados en el sistema. Aquí todo es sencillo. Decimos que la carpeta con todos los encabezados debe estar en el directorio include respecto al lugar de instalación.

install(DIRECTORY include/mylib DESTINATION include)

A continuación, le decimos al sistema de compilación que queremos poder llamar el comando en proyectos de terceros. find_package(Mylib) y conseguir un objetivo Mylib::mylib.

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

El siguiente encantamiento debe entenderse así. Cuando en un proyecto paralelo llamamos al comando find_package(Mylib 1.2.3 REQUIRED), y al mismo tiempo la versión real de la biblioteca instalada será incompatible con la versión 1.2.3, CMake generará automáticamente un error. Es decir, no necesitarás realizar un seguimiento de las versiones 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)

Pruebas

Si las pruebas están explícitamente deshabilitadas con opción correspondiente o nuestro proyecto es un subproyecto, es decir, está conectado a otro proyecto de CMake usando el comando add_subdirectory, no bajamos más abajo en la jerarquía y el script, que describe los comandos para generar y ejecutar pruebas, simplemente no se inicia.

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

Документация

Tampoco se generará documentación en el caso de un subproyecto.

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

Caja de arena en línea

De manera similar, el subproyecto tampoco tendrá un sandbox en línea.

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

Script para pruebas (test/CMakeLists.txt)

pruebas

En primer lugar, encontramos un paquete con el marco de prueba deseado (reemplácelo por su favorito).

find_package(doctest 2.3.3 REQUIRED)

Creamos nuestro archivo ejecutable con pruebas. Generalmente, directamente al binario ejecutable, agrego solo el archivo en el que estará la función. main.

add_executable(mylib-unit-tests test_main.cpp)

Y los archivos en los que se describen las pruebas en sí, los agrego más adelante. Pero no es necesario hacerlo.

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

Conectamos dependencias. Tenga en cuenta que adjuntamos solo los objetivos de CMake que necesitábamos a nuestro binario y no llamamos al comando target_include_directories. Encabezados del marco de prueba y de nuestro Mylib::mylib, así como las opciones de compilación (en nuestro caso, el estándar del lenguaje C++) rastreadas junto con estos objetivos.

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

Finalmente, creamos un objetivo ficticio cuya "compilación" es equivalente a ejecutar pruebas y agregamos este objetivo a la compilación predeterminada (esto es responsabilidad del atributo ALL). Esto significa que la compilación predeterminada activará la ejecución de las pruebas, lo que significa que nunca nos olvidaremos de ejecutarlas.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Покрытие

A continuación, active la medición de cobertura de código, si está configurada la opción correspondiente. No entraré en detalles porque están más relacionados con la herramienta de medición de cobertura que con CMake. Sólo es importante tener en cuenta que los resultados crearán una meta. coverage, con lo cual conviene empezar a medir la 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 de documentación (doc/CMakeLists.txt)

Doxigeno encontrado.

find_package(Doxygen)

A continuación, comprobamos si la variable con el idioma la establece el usuario. Si es así, entonces no lo tocamos, si no, tomamos ruso. Luego configuramos los archivos del sistema Doxygen. Todas las variables necesarias, incluido el idioma, llegan allí durante el proceso de configuración (ver. команду configure_file).

Luego creamos una meta. doc, que comenzará a generar documentación. Dado que generar documentación no es la mayor necesidad en el proceso de desarrollo, el objetivo no se incluirá de forma predeterminada, deberá ejecutarse explícitamente.

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 línea (en línea/CMakeLists.txt)

Aquí encontramos el tercer Python y creamos un objetivo. wandbox, que genera una solicitud correspondiente a la API del servicio Caja de varitay lo envía. En respuesta, aparece un enlace al sandbox terminado.

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

proyecto afuera

Ahora veamos cómo usarlo todo.

asamblea

La construcción de este proyecto, como cualquier otro proyecto en el sistema de construcción CMake, consta de dos etapas:

Generación

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

Si el comando anterior no funcionó debido a una versión anterior de CMake, intente omitir -S:

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

Más sobre opciones.

construcción del proyecto

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

Más sobre los objetivos de la asamblea.

Opciones

MYLIB_COVERAGE

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

Incluye objetivo coverage, con el que podrás empezar a medir la cobertura del código con pruebas.

MYLIB_TESTING

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

Proporciona la opción de desactivar la compilación y el objetivo de la prueba unitaria. check. Como resultado, la medición de la cobertura del código mediante pruebas se desactiva (consulte MYLIB_COVERAGE).

Además, las pruebas se desactivan automáticamente si el proyecto está conectado a otro proyecto como un subproyecto mediante el comando add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

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

Cambia el idioma de la documentación que genera el destino. doc al dado. Para obtener una lista de idiomas disponibles, consulte Sitio web de Doxygen.

El ruso está habilitado de forma predeterminada.

objetivos de montaje

Por defecto

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

Si no se especifica el objetivo (que es equivalente al objetivo all), recopila todo lo que es posible y también llama al objetivo check.

pruebas-unitarias-mylib

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

Compila pruebas unitarias. Habilitado de forma predeterminada.

comprobar

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

Ejecuta pruebas unitarias construidas (compiladas si no ya). Habilitado de forma predeterminada.

См. также mylib-unit-tests.

cobertura

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

Analiza las pruebas unitarias en ejecución (ejecuta, si aún no) para la cobertura del código mediante pruebas que utilizan el programa. gcovr.

El resultado de la cobertura se verá 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%
------------------------------------------------------------------------------

El objetivo está disponible solo cuando la opción está habilitada. MYLIB_COVERAGE.

См. также check.

doc

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

Inicia la generación de documentación para el código utilizando el sistema. Doxygen.

caja de varitas

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

La respuesta del servicio se parece a esto:

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

El servicio se utiliza para esto. Caja de varita. No sé qué servidores de goma tienen, pero creo que no deberías abusar de esta característica.

Примеры

Construir proyecto en modo de depuración con medición de cobertura

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

Instalar un proyecto sin preconstrucción ni pruebas

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

Construido en modo de lanzamiento por el compilador especificado

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

Generación de documentación en inglés.

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

Instrumentos

  1. CMake 3.13

    De hecho, la versión 3.13 de CMake solo es necesaria para ejecutar algunos de los comandos de la consola descritos en esta ayuda. Desde el punto de vista de la sintaxis de los scripts CMake, la versión 3.8 es suficiente si la generación se llama de otras formas.

  2. Biblioteca de pruebas prueba

    Las pruebas se pueden desactivar (ver опцию MYLIB_TESTING).

  3. Doxygen

    Para cambiar el idioma en el que se generará la documentación, existe una opción MYLIB_DOXYGEN_LANGUAGE.

  4. Intérprete de PL 3 Python

    Para generación automática cajas de arena en línea.

Análisis estático

Con la ayuda de CMake y un par de buenas herramientas, puede proporcionar análisis estático con un mínimo de manipulación.

Comprobación de CPP

Soporte para herramienta de análisis estático integrada en CMake Comprobación de CPP.

Para hacer esto, use la opción CMAKE_CXX_CPPCHECK:

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

Después de eso, el análisis estático se iniciará automáticamente cada vez durante la compilación y recompilación de fuentes. No es necesario hacer nada adicional.

Sonido metálico

Con una herramienta maravillosa scan-build También puedes ejecutar análisis estático en poco tiempo:

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

Aquí, a diferencia del caso con Cppcheck, es necesario ejecutar la compilación cada vez scan-build.

Epílogo

CMake es un sistema muy potente y flexible que le permite implementar funciones para todos los gustos y colores. Y, aunque la sintaxis a veces deja mucho que desear, el diablo todavía no es tan terrible como lo pintan. Utilice el sistema de compilación CMake en beneficio de la sociedad y la salud.

Descargar plantilla de proyecto

Fuente: habr.com

Añadir un comentario