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 dependencias, realizar análisis estático, medir el rendimiento, recopilar cobertura, generar documentación, etc. Y realmente disfruto de CMake, porque me permite hacer todo lo que quiero.

Muchos critican a CMake, y a menudo con razón, pero si se analiza, no es tan malo, y en los últimos tiempos de hecho es bastante bueno, y la dirección de su desarrollo es bastante positiva.

En esta nota quiero contar cómo es bastante simple organizar una biblioteca de encabezados en C++ dentro de un sistema CMake para obtener la siguiente funcionalidad:

  1. Compilación;
  2. Autocorrección de pruebas;
  3. Medición de cobertura de código;
  4. Instalación;
  5. Autodocumentación;
  6. Generación de un sandbox en línea;
  7. Análisis estático.

Quien ya tenga experiencia en C++ y CMake puede simplemente descargar la plantilla del proyecto y comenzar a usarla.


Contenido

  1. El proyecto desde adentro
    1. Estructura del proyecto
    2. El archivo principal de CMake (.\/CMakeLists.txt)
      1. Información sobre el proyecto
      2. Opciones del proyecto
      3. Opciones de compilación
      4. Objetivo principal
      5. Instalación
      6. Pruebas
      7. La documentación
      8. Sandbox en línea
    3. Script para pruebas (test\/CMakeLists.txt)
      1. Pruebas
      2. Cobertura
    4. Script para documentación (doc\/CMakeLists.txt)
    5. Script para sandbox en línea (online\/CMakeLists.txt)
  2. El proyecto desde afuera
    1. Compilación
      1. Generación
      2. Compilación
    2. Opciones
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Objetivos de compilación
      1. funciona hasta que se finalice manualmente. Por lo tanto, puede ser útil la opción
      2. mylib-unit-tests
      3. check
      4. coverage
      5. doc
      6. wandbox
    4. Ejemplos
  3. Herramientas
  4. Análisis estático
  5. Póscrito

El proyecto desde adentro

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

Principalmente, se hablará sobre cómo organizar los scripts de CMake, por lo que se analizarán en detalle. Los demás archivos cada interesado puede ver directamente en la página del proyecto de plantilla.

El archivo principal de CMake (.\/CMakeLists.txt)

Información sobre el proyecto

Primero, se debe requerir la versión necesaria del sistema CMake. CMake está evolucionando, se están cambiando las firmas de los comandos, el comportamiento en diferentes condiciones. Para que CMake entienda de inmediato qué queremos, debemos especificar nuestras demandas desde el principio.

cmake_minimum_required(VERSION 3.13)

Luego, definimos nuestro proyecto, su nombre, versión, lenguajes utilizados y demás (ver el comando project).

En este caso, indicamos el lenguaje CXX (lo que significa C++), para que CMake no se confunda y no busque un compilador de lenguaje C (por defecto, CMake incluye dos lenguajes: C y C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Aquí también se puede verificar de inmediato si nuestro proyecto está incluido en otro proyecto como subproyecto. Esto será de gran ayuda en el futuro.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Opciones del proyecto

Consideraremos dos opciones.

La primera opción es MYLIB_TESTING — para desactivar las pruebas modulares. Esto puede ser necesario si estamos seguros de que las pruebas están correctas y solo queremos, por ejemplo, instalar o empaquetar nuestro proyecto. O si nuestro proyecto está incluido como subproyecto; en este caso, al usuario de nuestro proyecto no le interesa ejecutar nuestras pruebas. ¿No pruebas las dependencias que utilizas?

option(MYLIB_TESTING "Habilitar pruebas modulares" ON)

Además, crearemos una opción separada MYLIB_COVERAGE para medir la cobertura de código mediante pruebas, pero requerirá herramientas adicionales, por lo que deberá activarse explícitamente.

option(MYLIB_COVERAGE "Habilitar medición de cobertura de código mediante pruebas" OFF)

Opciones de compilación

Por supuesto, somos programadores de C++ geniales, así que queremos que el compilador ofrezca el máximo nivel de diagnóstico en tiempo de compilación. Ningún error pasará desapercibido.

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 completamente con el estándar del lenguaje C++. Por defecto, están habilitadas en CMake.

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

Objetivo principal

Nuestra biblioteca consiste únicamente en archivos de encabezado, lo que significa que no generamos ninguna salida en forma de bibliotecas estáticas o dinámicas. Por otro lado, para usar nuestra biblioteca externamente, debe estar instalada, ser detectable en el sistema y vincularse a su proyecto, además de que estos encabezados deben estar incluidos junto con tal vez algunas propiedades adicionales.

Para este propósito, creamos una biblioteca de interfaz.

add_library(mylib INTERFACE)

Vinculamos los encabezados a nuestra biblioteca de interfaz.

El uso moderno, elegante y juvenil de CMake implica que los encabezados, propiedades, etc. se transmiten a través de un único objetivo. Por lo tanto, es suficiente decir target_link_libraries(target PRIVATE dependency), y todos los encabezados asociados con el objetivo dependency, estarán disponibles para los archivos fuente pertenecientes al objetivo target. Y no se requieren ninguna [target_]include_directoriesEsto se demostrará a continuación al analizar el script de CMake para pruebas unitarias.

También es importante notar las llamadas expressiones generadoras: $.

Este comando asocia los encabezados necesarios con nuestra biblioteca de interfaz, de tal forma que, si nuestra biblioteca se conecta a algún objetivo dentro de una misma jerarquía de CMake, se asociarán con ella los encabezados del directorio ${CMAKE_CURRENT_SOURCE_DIR}/include, y si nuestra biblioteca está instalada en el sistema y se conecta en otro proyecto mediante el comando find_package, se asociarán con ella los encabezados desde el directorio include con respecto al directorio de instalación.

target_include_directories(mylib INTERFACE
    $
    $
)

Establezcamos el estándar del lenguaje. Por supuesto, el más reciente. Además, no solo habilitamos el estándar, sino que lo extendemos a quienes usarán nuestra biblioteca. Esto se logra a través de que la propiedad establecida tiene la categoría INTERFACE (vea el comando target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Creemos un alias para nuestra biblioteca. Además, para darle un toque especial estará en un «espacio de nombres». Esto será útil cuando nuestra biblioteca tenga diferentes módulos, y podamos vincularlos de forma independiente. Como en Boost, por ejemplo..

add_library(Mylib::mylib ALIAS mylib)

Instalación

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

install(DIRECTORY include/mylib DESTINATION include)

Luego informamos al sistema de construcción que queremos poder en proyectos externos llamar al comando find_package(Mylib) y obtener el objetivo Mylib::mylib.

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

El siguiente hechizo debe entenderse así. Cuando en un proyecto externo llamemos al comando find_package(Mylib 1.2.3 REQUIRED), y la versión real de la biblioteca instalada es incompatible con la versión 1.2.3, CMake generará automáticamente un error. Es decir, no será necesario estar pendiente 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 desactivadas explícitamente mediante la opción correspondiente o nuestro proyecto es un subproyecto, es decir, está conectado a otro proyecto CMake mediante el comando add_subdirectory, no avanzamos más en la jerarquía, y el script que contiene los comandos para generar y ejecutar pruebas simplemente no se ejecuta.

if(NOT MYLIB_TESTING)
    message(STATUS "La prueba del proyecto Mylib está desactivada")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib no se prueba en modo submódulo")
else()
    add_subdirectory(test)
endif()

La documentación

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

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

Sandbox en línea

De manera similar, tampoco habrá un entorno en línea para el subproyecto.

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

Script para pruebas (test\/CMakeLists.txt)

Pruebas

Lo primero que haremos es encontrar el paquete con el framework de prueba necesario (sustitúyelo por tu favorito).

find_package(doctest 2.3.3 REQUIRED)

Creamos nuestro archivo ejecutable con las pruebas. Generalmente, solo añado en el binario ejecutable el archivo que contiene la función main.

add_executable(mylib-unit-tests test_main.cpp)

Y los archivos que describen las propias pruebas los añado más tarde. Pero no es necesario hacerlo de esa manera.

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

Conectamos las dependencias. Ten en cuenta que solo hemos vinculado los objetivos de CMake que necesitamos a nuestro binario, y no hemos llamado al comando target_include_directories. Los encabezados del framework de pruebas y de nuestro Mylib::mylib, así como los parámetros de compilación (en nuestro caso, esto es la norma del lenguaje C++) vinieron 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 la ejecución de pruebas, y añadimos este objetivo a la compilación por defecto (esto lo maneja el atributo , o). Esto significa que la compilación por defecto inicia la ejecución de pruebas, es decir, nunca olvidaremos ejecutarlas.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Cobertura

A continuación, habilitamos la medición de cobertura de código, si se ha especificado la opción correspondiente. No profundizaré en los detalles, ya que se relacionan más con la herramienta de medición de cobertura que con CMake. Solo es importante señalar que como resultado se creará un objetivo coverage, que permite iniciar fácilmente la medición de cobertura.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "La medición de la cobertura de código por pruebas está habilitada")

    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 "Se requiere el programa gcovr para medir la cobertura de código por pruebas")
endif()

Script para documentación (doc\/CMakeLists.txt)

Encontramos Doxygen.

find_package(Doxygen)

Luego verificamos si el usuario ha establecido una variable con el idioma. Si es así, la dejamos como está; si no, tomamos el ruso. Luego configuramos los archivos del sistema Doxygen. Todas las variables necesarias, incluido el idioma, se incluyen en el proceso de configuración (ver el comando configure_file).

Después creamos un objetivo doc, que ejecutará la generación de documentación. Dado que la generación de documentación no es una necesidad urgente en el proceso de desarrollo, por defecto el objetivo no estará habilitado y tendrá que ejecutarse explícitamente.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Ruso)
    endif()
    message(STATUS "La documentación de Doxygen se generará en ${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 (online\/CMakeLists.txt)

Aquí encontramos el tercer Python y creamos un objetivo wandbox, que genera una solicitud correspondiente a la API del servicio Wandbox, y la envía. En respuesta, recibimos un enlace a la caja de arena lista.

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 "Se requiere un intérprete de Python de la versión 3 para crear la caja de arena en línea")
endif()

El proyecto desde afuera

Ahora veamos cómo usar todo esto.

Compilación

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 ruta/a/fuentes -B ruta/a/directorio/de/construcción [opciones ...]

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

cmake ruta/a/fuentes -B ruta/a/directorio/de/construcción [opciones ...]

Más sobre las opciones.

Construcción del proyecto

cmake --build ruta/a/directorio/de/construcción [--target objetivo]

Más sobre los objetivos de construcción.

Opciones

MYLIB_COVERAGE

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

Habilita el objetivo coverage, que permite ejecutar la medición de la cobertura de código con pruebas.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [otras opciones ...]

Proporciona la opción de deshabilitar la compilación de pruebas unitarias y el objetivo check. Como resultado, se desactiva la medición de la cobertura de código con pruebas (ver MYLIB_COVERAGE).

También se desactiva la prueba automáticamente si el proyecto se incluye en otro proyecto como subproyecto mediante el comando add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=Spanish [otras opciones ...]

Cambia el idioma de la documentación generada por el objetivo doc al especificado. La lista de idiomas disponibles se encuentra en el sitio web del sistema Doxygen..

Por defecto está habilitado el ruso.

Objetivos de compilación

funciona hasta que se finalice manualmente. Por lo tanto, puede ser útil la opción

cmake --build ruta/a/directorio/de/construcción
cmake --build ruta/a/directorio/de/construcción --target all

Si no se especifica un objetivo (lo cual es equivalente al objetivo todo), compila todo lo que se pueda, y también invoca el objetivo check.

mylib-unit-tests

cmake --build ruta/a/directorio/de/construcción --target mylib-unit-tests

Compila pruebas unitarias. Esto está habilitado por defecto.

check

cmake --build ruta/a/directorio/de/construcción --target check

Ejecuta las pruebas unitarias compiladas (compila si aún no se han compilado). Esto está habilitado por defecto.

Ver también mylib-unit-tests.

coverage

cmake --build ruta/a/directorio/de/construcción --target coverage

Analiza las pruebas unitarias ejecutadas (las ejecuta si aún no se han ejecutado) para medir la cobertura de código de pruebas usando la herramienta gcovr.

La salida de la cobertura se verá aproximadamente así:

------------------------------------------------------------------------------
                           Informe de Cobertura de Código GCC
Directorio: /ruta/a/cmakecpptemplate/include/
------------------------------------------------------------------------------
Archivo                                       Líneas    Ejecución  Cobertura   Faltantes
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
TOTAL                                          2       2   100%
------------------------------------------------------------------------------

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

Ver también check.

doc

cmake --build ruta/a/directorio/de/construcción --target doc

Inicia la generación de documentación del código mediante el sistema Doxygen.

wandbox

cmake --build ruta/a/directorio/de/construcción --target wandbox

La respuesta del servicio se verá aproximadamente así:

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

Para esto se utiliza el servicio Wandbox. No sé cuán flexibles son sus servidores, pero creo que no deberías abusar de esta posibilidad.

Ejemplos

La construcción del proyecto en modo de depuración con medición de cobertura

cmake -S ruta/a/fuentes -B ruta/a/directorio/de/construcción -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build ruta/a/directorio/de/construcción --target coverage --parallel 16

Instalación del proyecto sin compilación y pruebas previas

cmake -S ruta/a/los/orígenes -B ruta/a/directorio/de/compilación -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=ruta/a/directorio/de/instalación
cmake --build ruta/a/directorio/de/compilación --target install

Compilación en modo de lanzamiento con el compilador especificado

cmake -S ruta/a/los/orígenes -B ruta/a/directorio/de/compilación -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=ruta/a/directorio/donde/están/las/dependencias
cmake --build ruta/a/directorio/de/compilación --parallel 4

Generación de documentación en inglés

cmake -S ruta/a/los/orígenes -B ruta/a/directorio/de/compilación -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build ruta/a/directorio/de/compilación --target doc

Herramientas

  1. CMake 3.13

    De hecho, se requiere la versión CMake 3.13 solo para ejecutar ciertos comandos de consola descritos en esta referencia. Desde el punto de vista de la sintaxis de los scripts de CMake, es suficiente la versión 3.8 si se invoca la generación de otras maneras.

  2. Biblioteca de pruebas doctest

    Las pruebas se pueden desactivar (ver opción MYLIB_TESTING).

  3. Doxygen

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

  4. Intérprete de lenguaje de programación Python 3

    Para la generación automática sandbox en línea.

Análisis estático

Con CMake y un par de buenas herramientas, se puede asegurar análisis estático con un mínimo de esfuerzo.

Cppcheck

CMake tiene soporte integrado para la herramienta de análisis estático Cppcheck.

Para ello, se debe usar la opción CMAKE_CXX_CPPCHECK:

cmake -S ruta/a/los/orígenes -B ruta/a/directorio/de/compilación -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-I ruta/a/los/orígenes/include"

Después de esto, el análisis estático se ejecutará automáticamente cada vez que se compile o recompilen los orígenes. No es necesario hacer nada adicional.

Clang

Con esta maravillosa herramienta scan-build también se puede realizar un análisis estático en un abrir y cerrar de ojos:

scan-build cmake -S ruta/a/los/orígenes -B ruta/a/directorio/de/compilación -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build ruta/a/directorio/de/compilación

Aquí, a diferencia del caso con Cppcheck, se requiere ejecutar la compilación a través de scan-build.

Póscrito

CMake es un sistema muy potente y flexible que permite implementar funcionalidades de cualquier tipo. Y, aunque la sintaxis a veces deja mucho que desear, no es tan terrible como la pintan. Utilicen el sistema de compilación CMake para el beneficio de la sociedad y en beneficio de la salud.

→ Descargar plantilla de proyecto

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster