
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:
- Compilación;
- Autocorrección de pruebas;
- Medición de cobertura de código;
- Instalación;
- Autodocumentación;
- Generación de un sandbox en línea;
- Análisis estático.
Quien ya tenga experiencia en C++ y CMake puede simplemente y comenzar a usarla.
Contenido
.
├── 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.cppPrincipalmente, 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 .
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 ).
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)
Consideraremos dos opciones.
La primera opción es — 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 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)
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()
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 , 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 .
También es importante notar las llamadas .
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 , 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 ).
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. .
add_library(Mylib::mylib ALIAS mylib)
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)
Si las pruebas están desactivadas explícitamente mediante o nuestro proyecto es un subproyecto, es decir, está conectado a otro proyecto CMake mediante el comando , 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 tampoco se generará en el caso de un subproyecto.
if(NOT IS_SUBPROJECT)
add_subdirectory(doc)
endif()
De manera similar, tampoco habrá un entorno en línea para el subproyecto.
if(NOT IS_SUBPROJECT)
add_subdirectory(online)
endif()
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)
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 , 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()
.
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 ).
Después creamos un objetivo , 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 ()
Aquí encontramos el tercer Python y creamos un objetivo , que genera una solicitud correspondiente a la API del servicio , 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()
Ahora veamos cómo usar todo esto.
La construcción de este proyecto, como cualquier otro proyecto en el sistema de construcción CMake, consta de dos etapas:
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 ...]
.
cmake --build ruta/a/directorio/de/construcción [--target objetivo].
cmake -S ... -B ... -DMYLIB_COVERAGE=ON [otras opciones ...]Habilita el objetivo , que permite ejecutar la medición de la cobertura de código con pruebas.
cmake -S ... -B ... -DMYLIB_TESTING=OFF [otras opciones ...]Proporciona la opción de deshabilitar la compilación de pruebas unitarias y el objetivo . Como resultado, se desactiva la medición de la cobertura de código con pruebas (ver ).
También se desactiva la prueba automáticamente si el proyecto se incluye en otro proyecto como subproyecto mediante el comando .
cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=Spanish [otras opciones ...]Cambia el idioma de la documentación generada por el objetivo al especificado. La lista de idiomas disponibles se encuentra en .
Por defecto está habilitado el ruso.
cmake --build ruta/a/directorio/de/construcción
cmake --build ruta/a/directorio/de/construcción --target allSi no se especifica un objetivo (lo cual es equivalente al objetivo todo), compila todo lo que se pueda, y también invoca el objetivo .
cmake --build ruta/a/directorio/de/construcción --target mylib-unit-testsCompila pruebas unitarias. Esto está habilitado por defecto.
cmake --build ruta/a/directorio/de/construcción --target checkEjecuta las pruebas unitarias compiladas (compila si aún no se han compilado). Esto está habilitado por defecto.
Ver también .
cmake --build ruta/a/directorio/de/construcción --target coverageAnaliza 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 .
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. .
Ver también .
cmake --build ruta/a/directorio/de/construcción --target docInicia la generación de documentación del código mediante el sistema .
cmake --build ruta/a/directorio/de/construcción --target wandboxLa respuesta del servicio se verá aproximadamente así:
{
"permlink" : "QElvxuMzHgL9fqci",
"status" : "0",
"url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}Para esto se utiliza el servicio . No sé cuán flexibles son sus servidores, pero creo que no deberías abusar de esta posibilidad.
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 16Instalació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 installCompilació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 4Generació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
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.
Biblioteca de pruebas
Las pruebas se pueden desactivar (ver ).
Para cambiar el idioma en el que se generará la documentación, hay una opción disponible .
Intérprete de lenguaje de programación
Para la generación automática .
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 .
Para ello, se debe usar la opción :
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 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ónAquí, a diferencia del caso con Cppcheck, se requiere ejecutar la compilación a través de scan-build.
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.
→
Fuente: habr.com
