CMake y C ++: hermanos para siempre

Amistad para siempre


Durante el proceso de 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 a CMake, porque me permite hacer todo lo que quiero.


Muchos regañan a CMake, y a menudo con justicia, pero si lo miras, no todo es tan malo, y últimamente es muy bueno , y la dirección del desarrollo es bastante positiva.


En este artículo quiero decir cuán simple es organizar una biblioteca de encabezado C ++ en el sistema CMake para obtener la siguiente funcionalidad:


  1. Asamblea;
  2. Pruebas de inicio automático;
  3. Medición de la cobertura del código;
  4. Instalación;
  5. Documentación automática
  6. Generación de sandbox en línea;
  7. Análisis estático

Quien ya entienda los pros y s-make puede simplemente descargar la plantilla del proyecto y comenzar a usarla.


Contenido


  1. Proyecto de adentro hacia afuera
    1. Estructura del proyecto
    2. Archivo CMake principal (./CMakeLists.txt)
      1. Información del proyecto
      2. Opciones de proyecto
      3. Opciones de compilación
      4. Objetivo principal
      5. Instalación
      6. Pruebas
      7. La documentación
      8. Caja de arena en línea
    3. Script para pruebas (test / CMakeLists.txt)
      1. Prueba
      2. Cobertura
    4. Script para documentación (doc / CMakeLists.txt)
    5. Script para un sandbox en línea (online / 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 la asamblea
      1. Por defecto
      2. mylib-unit-tests
      3. comprobar
      4. cobertura
      5. doc
      6. caja de varitas
    4. Ejemplos
  3. Las herramientas
  4. Análisis estático
  5. Epílogo


Proyecto de adentro hacia afuera



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 

Hablaremos principalmente sobre cómo organizar los scripts de CMake, para que sean analizados en detalle. Todos pueden ver el resto de los archivos directamente en la página de la plantilla del proyecto .



Archivo CMake principal (./CMakeLists.txt)



Información del proyecto


En primer lugar, debe solicitar la versión correcta del sistema CMake. CMake está evolucionando, las firmas del equipo, el comportamiento en diferentes condiciones están cambiando. Para que CMake entienda de inmediato lo que queremos de él, necesitamos arreglar de inmediato nuestros requisitos para él.


 cmake_minimum_required(VERSION 3.13) 

Luego designamos nuestro proyecto, su nombre, versión, idiomas utilizados, etc. (vea project ).


En este caso, especificamos el lenguaje CXX (que significa C ++) para que CMake no se esfuerce y no busque el compilador del lenguaje C (de manera predeterminada, CMake incluye dos idiomas: C y C ++).


 project(Mylib VERSION 1.0 LANGUAGES CXX) 

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


 get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY) 


Opciones de proyecto


Ofrecemos dos opciones.


La primera opción, MYLIB_TESTING , para desactivar 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 prueba las dependencias que usa?


 option(MYLIB_TESTING "  " ON) 

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


 option(MYLIB_COVERAGE "    " OFF) 


Opciones de compilación


Por supuesto, somos geniales más programadores, por lo que queremos el nivel máximo de diagnóstico de tiempo de compilación del compilador. No se deslizará un solo mouse.


 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 deshabilitaremos las extensiones para cumplir completamente con el estándar del lenguaje C ++. Por defecto, están incluidos en CMake.


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


Objetivo principal


Nuestra biblioteca consta solo de archivos de encabezado, lo que significa que no tenemos ningún escape en forma de bibliotecas estáticas o dinámicas. Por otro lado, para usar nuestra biblioteca en el exterior, debe instalarla, debe poder encontrarla en el sistema y conectarla a su proyecto, y al mismo tiempo estos mismos encabezados, así como, posiblemente, algunos adicionales. propiedades.


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


 add_library(mylib INTERFACE) 

Vincula los encabezados a nuestra biblioteca front-end.


El uso moderno, moderno y juvenil de CMake implica que los encabezados, propiedades, etc. transmitido a través de un solo propósito. Por lo tanto, basta con decir target_link_libraries(target PRIVATE dependency) , y todos los encabezados asociados con el objetivo de dependency estarán disponibles para las fuentes que pertenecen al objetivo de target . Y no se [target_]include_directories . Esto se demostrará a continuación al analizar el 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 front-end, y si nuestra biblioteca está conectada a un destino dentro de la misma jerarquía CMake, los encabezados del directorio ${CMAKE_CURRENT_SOURCE_DIR}/include se asociarán con él, y si los nuestros Dado que la biblioteca está instalada en el sistema y conectada a otro proyecto utilizando el find_package , los encabezados del directorio de include relativos al directorio de instalación se asociarán con él.


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

Establecer el idioma estándar. Por supuesto, el ú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 debido al hecho de que la propiedad set tiene la categoría INTERFACE (consulte el comando target_compile_features ).


 target_compile_features(mylib INTERFACE cxx_std_17) 

Creamos un alias para nuestra biblioteca. Además, por belleza, estará en un "espacio de nombres" especial. Esto será útil cuando aparezcan diferentes módulos en nuestra biblioteca, y vamos a conectarlos independientemente uno del otro. Como en Boost, por ejemplo .


 add_library(Mylib::mylib ALIAS mylib) 


Instalación


Instalando nuestros encabezados en el sistema. Todo es simple aquí. Decimos que la carpeta con todos los encabezados debe estar en el directorio de include relación con la ubicación de instalación.


 install(DIRECTORY include/mylib DESTINATION include) 

A continuación, informamos al sistema de compilación que queremos poder llamar a find_package(Mylib) en proyectos de terceros 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 de la siguiente manera. Cuando llamamos a find_package(Mylib 1.2.3 REQUIRED) en un proyecto de terceros, y en este caso 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á seguir 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 se desactivan explícitamente usando la opción apropiada o si nuestro proyecto es un subproyecto, es decir, conectado a otro proyecto CMake usando el add_subdirectory , no add_subdirectory en la jerarquía, y el script que describe los comandos para generar y ejecutar las pruebas simplemente no comienza .


 if(NOT MYLIB_TESTING) message(STATUS "  Mylib ") elseif(IS_SUBPROJECT) message(STATUS "Mylib     ") else() add_subdirectory(test) endif() 


La documentación


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


Del mismo modo, el subproyecto tampoco tendrá sandboxes en línea.


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


Script para pruebas (test / CMakeLists.txt)



Prueba


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


 find_package(doctest 2.3.3 REQUIRED) 

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


 add_executable(mylib-unit-tests test_main.cpp) 

Y los archivos que describen las pruebas en sí se agregan más tarde. Pero esto no es necesario.


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

Conectamos dependencias. Tenga en cuenta que solo vinculamos los objetivos CMake que necesitábamos con nuestro binario, y no llamamos al comando target_include_directories . Los encabezados del marco de prueba y de nuestro Mylib::mylib , así como los parámetros de compilación (en nuestro caso, este es el estándar del lenguaje C ++) se rastrearon junto con estos objetivos.


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

Finalmente, cree un objetivo ficticio, cuyo "ensamblaje" sea equivalente a ejecutar pruebas, y agregue este objetivo al ensamblaje predeterminado (el atributo ALL es responsable de esto). Esto significa que el ensamblaje por defecto inicia el lanzamiento de las pruebas, es decir, nunca olvidaremos ejecutarlas.


 add_custom_target(check ALL COMMAND mylib-unit-tests) 


Cobertura


A continuación, habilitamos la medición de la cobertura del código, si se especifica la opción correspondiente. No entraré en detalles, porque se relacionan más con la herramienta para medir la cobertura que con CMake. Solo es importante tener en cuenta que, en función de los resultados, se creará un objetivo de coverage , con el cual es conveniente comenzar 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 para documentación (doc / CMakeLists.txt)


Doxygen encontrado .


 find_package(Doxygen) 

A continuación, verificamos si el usuario ha establecido la variable de idioma. En caso afirmativo, no toque, si no, tome ruso. Luego configure los archivos del sistema Doxygen. Todas las variables necesarias, incluido el idioma, llegan allí durante el proceso de configuración (consulte el configure_file ).


Luego creamos el objetivo del doc , que comenzará la generación de documentación. Dado que la generación de documentación no es la mayor necesidad en el proceso de desarrollo, por defecto el objetivo no estará habilitado, deberá iniciarse 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 un sandbox en línea (online / CMakeLists.txt)


Aquí encontramos el tercer Python y creamos un objetivo de wandbox que genera una solicitud que coincide con la API del servicio Wandbox y la 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 considere cómo usarlo todo.



Asamblea


El ensamblaje de este proyecto, como cualquier otro proyecto en el sistema de ensamblaje CMake, consta de dos etapas:



Generación


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

Si el comando anterior no funcionó debido a la versión anterior de CMake, intente omitir -S :
 cmake // -B /// [ ...] 

Más sobre opciones .



Asamblea de proyecto


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

Lea más sobre los objetivos de montaje .



Opciones



MYLIB_COVERAGE


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

Incluye un objetivo de coverage , con el que puede comenzar a medir la cobertura de código con pruebas.



MYLIB_TESTING


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

Proporciona la capacidad de apagar el conjunto de prueba de la unidad y el objetivo de check . Como resultado, la medición de la cobertura del código mediante pruebas se desactiva (consulte MYLIB_COVERAGE ).


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



MYLIB_DOXYGEN_LANGUAGE


 cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [  ...] 

Cambia el lenguaje de documentación que el objetivo de doc genera al especificado. Para obtener una lista de los idiomas disponibles, consulte el sitio web del sistema Doxygen .


Por defecto, el ruso está habilitado.



Objetivos de la asamblea



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 general), recoge todo lo que es posible y también llama al objetivo de check .



mylib-unit-tests


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

Compila pruebas unitarias. Habilitado por defecto.



comprobar


 cmake --build /// --target check 

Ejecuta pruebas de unidad recolectadas (recolecta, si no todavía). Habilitado por defecto.


Ver también mylib-unit-tests .



cobertura


 cmake --build /// --target coverage 

Analiza las pruebas unitarias en ejecución (se ejecuta, si no todavía) para cubrir el código con pruebas usando el programa gcovr .


El escape del revestimiento se verá más o menos 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 solo está disponible cuando MYLIB_COVERAGE .


Ver también check .



doc


 cmake --build /// --target doc 

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



caja de varitas


 cmake --build /// --target wandbox 

La respuesta del servicio se ve así:


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

Para hacer esto, use el servicio Wandbox . No sé cómo tienen los servidores de caucho, pero creo que no se debe aprovechar esta oportunidad.



Ejemplos


Montaje del 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 

Instalación del proyecto sin montaje y pruebas preliminares.


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

Compilación 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 


Las herramientas


  1. CMake 3.13


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


  2. Biblioteca de pruebas de Doctest


    Las pruebas se pueden deshabilitar (consulte la MYLIB_TESTING ).


  3. Doxygen


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


  4. Python 3 Intérprete


    Para generar automáticamente sandboxes en línea .




Análisis estático


Con CMake y un par de buenas herramientas, puede proporcionar análisis estático con movimientos mínimos del cuerpo.


Cppcheck


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


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 las fuentes. No necesitas hacer nada extra.


Clang


Con la maravillosa herramienta de scan-build , también puede ejecutar análisis estático en dos cuentas:


 scan-build cmake -S // -B /// -DCMAKE_BUILD_TYPE=Debug scan-build cmake --build /// 

Aquí, a diferencia del caso de Cppcheck, debe ejecutar el ensamblaje cada vez a través de scan-build .



Epílogo


CMake es un sistema muy potente y flexible que le permite implementar funcionalidades 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 está pintado. Utilice el sistema de construcción CMake en beneficio de la sociedad y la salud.




Descargar plantilla de proyecto

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


All Articles