Guía completa de CMake. Segunda parte: sistema de compilación


Introduccion


Este artículo analiza el uso del sistema de compilación CMake utilizado en una gran cantidad de proyectos C / C ++. Se recomienda encarecidamente que lea la primera parte del manual para evitar malentendidos sobre la sintaxis del lenguaje CMake, que aparece explícitamente en todo el artículo.


Lanzamiento de CMake


Los siguientes son ejemplos del uso del lenguaje CMake que debe practicar. Experimente con el código fuente modificando los comandos existentes y agregando otros nuevos. Para ejecutar estos ejemplos, instale CMake desde el sitio web oficial .


Principio de funcionamiento


El sistema de compilación CMake es un contenedor sobre otras utilidades dependientes de la plataforma (por ejemplo, Ninja o Make ). Por lo tanto, en el proceso de ensamblaje en sí, no importa cuán paradójico pueda sonar, no participa directamente.


El sistema de compilación CMake acepta un archivo CMakeLists.txt con una descripción de las reglas de compilación en el lenguaje formal de CMake, y luego genera archivos de compilación intermedios y nativos en el mismo directorio aceptado en su plataforma.


Los archivos generados contendrán nombres específicos de utilidades del sistema, directorios y compiladores, mientras que los comandos CMake solo usan el concepto abstracto del compilador y no están vinculados a herramientas dependientes de la plataforma que difieren mucho en los diferentes sistemas operativos.


Comprobación de la versión de CMake


El comando cmake_minimum_required verifica la versión en ejecución de CMake: si es inferior al mínimo especificado, CMake termina con un error fatal. Un ejemplo que demuestra el uso típico de este comando al comienzo de cualquier archivo CMake:


 #     CMake: cmake_minimum_required(VERSION 3.0) 

Como se señaló en los comentarios, el comando cmake_minimum_required establece todos los indicadores de compatibilidad (consulte cmake_policy ). Algunos desarrolladores establecen intencionalmente una versión baja de CMake y luego ajustan la funcionalidad manualmente. Esto le permite admitir simultáneamente las versiones antiguas de CMake y, en algunos lugares, aprovechar las nuevas funciones.


Diseño del proyecto


Al comienzo de cualquier CMakeLists.txt debe especificar las características del proyecto con el equipo del proyecto para un mejor diseño con entornos integrados y otras herramientas de desarrollo.


 #    "MyProject": project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX) 

Vale la pena señalar que si se omite la palabra clave LANGUAGES , los idiomas predeterminados son C CXX . También puede deshabilitar la indicación de cualquier idioma escribiendo la palabra clave NONE como una lista de idiomas o simplemente dejando una lista vacía.


Ejecutar archivos de script


El comando de include reemplaza la línea de su llamada con el código del archivo especificado, actuando de manera similar al comando de include preprocesador C / C ++. Este ejemplo ejecuta el archivo de script MyCMakeScript.cmake comando descrito:


 message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]") #   `MyCMakeScript.cmake`  : include(MyCMakeScript.cmake) message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]") 

En este ejemplo, el primer mensaje notificará que la variable TEST_VARIABLE no se ha definido, sin embargo, si el script MyCMakeScript.cmake esta variable, el segundo mensaje ya informará sobre el nuevo valor de la variable de prueba. Por lo tanto, el archivo de script incluido por el comando de include no crea su propio alcance, que se mencionó en los comentarios al artículo anterior .


Compilación de archivos ejecutables.


El comando add_executable compila el archivo ejecutable con el nombre de la lista de origen. Es importante tener en cuenta que el nombre del archivo final depende de la plataforma de destino (por ejemplo, <ExecutableName>.exe o simplemente <ExecutableName> ). Un ejemplo típico de llamar a este comando:


 #    "MyExecutable"  #  "ObjectHandler.c", "TimeManager.c"  "MessageGenerator.c": add_executable(MyExecutable ObjectHandler.c TimeManager.c MessageGenerator.c) 

Compilación de la biblioteca


El comando add_library compila la biblioteca con la vista y el nombre especificados de la fuente. Es importante tener en cuenta que el nombre final de la biblioteca depende de la plataforma de destino (por ejemplo, lib<LibraryName>.a o <LibraryName>.lib ). Un ejemplo típico de llamar a este comando:


 #    "MyLibrary"  #  "ObjectHandler.c", "TimeManager.c"  "MessageConsumer.c": add_library(MyLibrary STATIC ObjectHandler.c TimeManager.c MessageConsumer.c) 

  • Las bibliotecas estáticas se definen mediante la palabra clave STATIC como el segundo argumento y son archivos de archivos de objetos asociados con archivos ejecutables y otras bibliotecas en tiempo de compilación;
  • Las bibliotecas dinámicas se especifican mediante la palabra clave SHARED como segundo argumento y son bibliotecas binarias cargadas por el sistema operativo durante la ejecución del programa;
  • Las bibliotecas modulares están definidas por la palabra clave MODULE como el segundo argumento y son bibliotecas binarias cargadas utilizando la técnica de ejecución por el propio ejecutable;
  • Las bibliotecas de objetos se definen por la palabra clave OBJECT como el segundo argumento y son un conjunto de archivos de objetos asociados con archivos ejecutables y otras bibliotecas en tiempo de compilación.

Agregar fuente al objetivo


Hay casos que requieren múltiples adiciones de archivos de origen al destino. Para hacer esto, se target_sources comando target_sources , que puede agregar orígenes al destino muchas veces.


El primer argumento para el comando target_sources es el nombre del objetivo previamente especificado usando los add_executable add_library o add_executable , y los argumentos siguientes son una lista de los archivos fuente que se agregarán.


Las llamadas repetidas al target_sources agregan los archivos fuente al destino en el orden en que fueron llamados, por lo que los dos bloques de código inferiores son funcionalmente equivalentes:


 #    "MyExecutable"   # "ObjectPrinter.c"  "SystemEvaluator.c": add_executable(MyExecutable ObjectPrinter.c SystemEvaluator.c) #    "MyExecutable"  "MessageConsumer.c": target_sources(MyExecutable MessageConsumer.c) #    "MyExecutable"  "ResultHandler.c": target_sources(MyExecutable ResultHandler.c) 

 #    "MyExecutable"   # "ObjectPrinter.c", "SystemEvaluator.c", "MessageConsumer.c"  "ResultHandler.c": add_executable(MyExecutable ObjectPrinter.c SystemEvaluator.c MessageConsumer.c ResultHandler.c) 

Archivos generados


La ubicación de los archivos de salida generados por los add_library add_executable y add_library se determina solo en la etapa de generación, sin embargo, esta regla se puede cambiar con varias variables que determinan la ubicación final de los archivos binarios:



Los archivos ejecutables siempre se consideran objetivos de ejecución, las bibliotecas estáticas se consideran objetivos de archivo y las bibliotecas modulares se consideran objetivos de biblioteca. Para las plataformas "no DLL", las bibliotecas dinámicas se consideran objetivos de la biblioteca, y para las "plataformas DLL", los objetivos de ejecución. Dichas variables no se proporcionan para las bibliotecas de objetos, ya que este tipo de bibliotecas se genera en las entrañas del directorio CMakeFiles .


Es importante tener en cuenta que todas las plataformas basadas en Windows, incluida Cygwin, se consideran "plataformas DLL".


Diseño de la biblioteca


El comando target_link_libraries biblioteca o ejecutable con otras bibliotecas proporcionadas. El primer argumento para este comando es el nombre del objetivo generado por los add_library add_executable o add_library , y los argumentos posteriores son los nombres de los objetivos de la biblioteca o las rutas completas a las bibliotecas. Un ejemplo:


 #    "MyExecutable"  #  "JsonParser", "SocketFactory"  "BrowserInvoker": target_link_libraries(MyExecutable JsonParser SocketFactory BrowserInvoker) 

Vale la pena señalar que las bibliotecas modulares no se pueden vincular con archivos ejecutables u otras bibliotecas, ya que están destinadas únicamente a la carga mediante técnicas de ejecución.


Trabaja con objetivos


Como se menciona en los comentarios, los objetivos en CMake también están sujetos a manipulación manual, aunque muy limitada.


Es posible controlar las propiedades de los objetivos diseñados para establecer el proceso de ensamblaje del proyecto. El comando get_target_property valor de la propiedad de destino en la variable proporcionada. Este ejemplo muestra el valor de la propiedad C_STANDARD objetivo C_STANDARD en la pantalla:


 #   "VALUE"   "C_STANDARD": get_target_property(VALUE MyTarget C_STANDARD) #      : message("'C_STANDARD' property is equal to [${VALUE}]") 

El comando set_target_properties establece las propiedades de destino especificadas en los valores especificados. Este comando acepta una lista de objetivos para los que se establecerán valores de propiedad, y luego la palabra clave PROPERTIES , seguida de una lista de la forma < > < > :


 #   'C_STANDARD'  "11", #   'C_STANDARD_REQUIRED'  "ON": set_target_properties(MyTarget PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON) 

El ejemplo anterior establece las propiedades de los objetivos MyTarget que afectan el proceso de compilación, a saber: al compilar el objetivo MyTarget CMake MyTarget compilador use el estándar C11. Todos los nombres de propiedades de destino conocidos se enumeran en esta página .


También es posible verificar objetivos previamente definidos utilizando la construcción if(TARGET <TargetName>) :


 #  "The target was defined!"   "MyTarget"  , #    "The target was not defined!": if(TARGET MyTarget) message("The target was defined!") else() message("The target was not defined!") endif() 

Agregar subproyectos


El comando add_subdirectory solicita a CMake que procese inmediatamente el archivo de subproyecto especificado. El siguiente ejemplo demuestra la aplicación del mecanismo descrito:


 #   "subLibrary"    , #       "subLibrary/build": add_subdirectory(subLibrary subLibrary/build) 

En este ejemplo, el primer argumento para el comando add_subdirectory es el subproyecto add_subdirectory , y el segundo argumento es opcional e informa a CMake sobre la carpeta destinada a los archivos generados del subproyecto incluido (por ejemplo, CMakeCache.txt y cmake_install.cmake ).


Vale la pena señalar que todas las variables del ámbito primario son heredadas por el directorio agregado, y todas las variables definidas y redefinidas en este directorio serán visibles solo para él (si la palabra clave PARENT_SCOPE no PARENT_SCOPE especificada por el argumento del comando set ). Esta característica fue mencionada en los comentarios al artículo anterior .


Paquete de búsqueda


El comando find_package encuentra y carga la configuración de un proyecto externo. En la mayoría de los casos, se utiliza para la vinculación posterior de bibliotecas externas como Boost y GSL . Este ejemplo llama al comando descrito para buscar la biblioteca GSL y luego vincular:


 #     "GSL": find_package(GSL 2.5 REQUIRED) #      "GSL": target_link_libraries(MyExecutable GSL::gsl) #      "GSL": target_include_directories(MyExecutable ${GSL_INCLUDE_DIRS}) 

En el ejemplo anterior, el comando find_package acepta el nombre del paquete como primer argumento y luego la versión requerida. La opción REQUIRED requiere imprimir un error fatal y finalizar CMake si no se encuentra el paquete requerido. Lo opuesto es la opción QUIET , que requiere que CMake continúe su trabajo, incluso si no se encontró el paquete.


A continuación, el MyExecutable vincula a la biblioteca GSL con el comando target_link_libraries utilizando la variable GSL::gsl , que encapsula la ubicación del GSL ya compilado.


Al final, se target_include_directories comando target_include_directories , que informa al compilador sobre la ubicación de los archivos de encabezado de la biblioteca GSL. Tenga en cuenta que la variable GSL_INCLUDE_DIRS se usa para GSL_INCLUDE_DIRS ubicación de los encabezados que describí (este es un ejemplo de la configuración del paquete importado).


Probablemente desee verificar el resultado de una búsqueda de paquetes si especificó la opción QUIET . Esto se puede hacer comprobando la <PackageName>_FOUND , que se determina automáticamente después de que se find_package comando find_package . Por ejemplo, si importa con éxito la configuración de GSL en su proyecto, la variable GSL_FOUND se convertirá en verdadera.


En general, el comando find_package tiene dos tipos de lanzamiento: modular y configuración. El ejemplo anterior aplicó una forma modular. Esto significa que cuando se llama al comando, CMake busca un archivo de script con la forma Find<PackageName>.cmake en el directorio CMAKE_MODULE_PATH , y luego lo inicia e importa todas las configuraciones necesarias (en este caso, CMake lanzó el archivo FindGSL.cmake estándar).


Formas de incluir encabezados


Puede informar al compilador sobre la ubicación de los encabezados incluidos mediante dos comandos: include_directories y target_include_directories . Usted decide cuál usar, sin embargo, vale la pena considerar algunas diferencias entre ellos (la idea se sugiere en los comentarios ).


El comando include_directories afecta el alcance del directorio. Esto significa que todos los directorios de encabezado especificados por este comando se usarán para todos los propósitos del CMakeLists.txt actual, así como para subproyectos procesados ​​(ver add_subdirectory ).


El comando target_include_directories afecta al objetivo especificado por el primer argumento, y no afecta a otros objetivos. El siguiente ejemplo demuestra la diferencia entre los dos comandos:


 add_executable(RequestGenerator RequestGenerator.c) add_executable(ResponseGenerator ResponseGenerator.c) #     "RequestGenerator": target_include_directories(RequestGenerator headers/specific) #    "RequestGenerator"  "ResponseGenerator": include_directories(headers) 

En los comentarios se menciona que en los proyectos modernos el uso de los link_libraries include_directories y link_libraries es indeseable. Una alternativa son los target_link_libraries target_include_directories y target_link_libraries que actúan solo en objetivos específicos y no en todo el alcance actual.


Proyecto de instalacion


El comando de instalación genera reglas de instalación para su proyecto. Este comando es capaz de trabajar con objetivos, archivos, carpetas y más. Primero, considere establecer metas.


Para establecer objetivos, debe pasar la palabra clave TARGETS como primer argumento de la función descrita, seguida de una lista de los objetivos que se establecerán y luego la palabra clave DESTINATION con la ubicación del directorio en el que se establecerán estos objetivos. Este ejemplo demuestra una configuración típica de objetivos:


 #   "TimePrinter"  "DataScanner"   "bin": install(TARGETS TimePrinter DataScanner DESTINATION bin) 

El proceso para describir la instalación de archivos es similar, excepto que en lugar de la TARGETS , especifique FILES . Un ejemplo que demuestra la instalación de archivos:


 #   "DataCache.txt"  "MessageLog.txt"   "~/": install(FILES DataCache.txt MessageLog.txt DESTINATION ~/) 

El proceso para describir la instalación de carpetas es similar, excepto que debe especificar DIRECTORY lugar de la palabra clave FILES . Es importante tener en cuenta que durante la instalación se copiará todo el contenido de la carpeta, y no solo su nombre. Un ejemplo de instalación de carpetas es el siguiente:


 #   "MessageCollection"  "CoreFiles"   "~/": install(DIRECTORY MessageCollection CoreFiles DESTINATION ~/) 

Después de completar el procesamiento CMake de todos sus archivos, puede instalar todos los objetos descritos con el sudo checkinstall (si CMake genera un Makefile ), o puede realizar esta acción con el entorno de desarrollo integrado que admite CMake.


Ejemplo visual del proyecto.


Esta guía no estaría completa sin demostrar un ejemplo real del uso del sistema de compilación CMake. Considere un diagrama de proyecto simple usando CMake como el único sistema de compilación:


 + MyProject - CMakeLists.txt - Defines.h - StartProgram.c + core - CMakeLists.txt - Core.h - ProcessInvoker.c - SystemManager.c 

El archivo de ensamblaje principal CMakeLists.txt describe la compilación de todo el programa: primero, se add_executable comando add_executable que compila el archivo ejecutable, luego se add_subdirectory comando add_subdirectory , que estimula el procesamiento del subproyecto, y finalmente, el archivo ejecutable está vinculado a la biblioteca compilada:


 #    CMake: cmake_minimum_required(VERSION 3.0) #   : project(MyProgram VERSION 1.0.0 LANGUAGES C) #      "MyProgram": add_executable(MyProgram StartProgram.c) #    "core/CMakeFiles.txt": add_subdirectory(core) #    "MyProgram"  #    "MyProgramCore": target_link_libraries(MyProgram MyProgramCore) #    "MyProgram"   "bin": install(TARGETS MyProgram DESTINATION bin) 

El archivo de ensamblaje principal llama al archivo core/CMakeLists.txt y compila la biblioteca estática MyProgramCore destinada a vincularse con el archivo ejecutable:


 #    CMake: cmake_minimum_required(VERSION 3.0) #      "MyProgramCore": add_library(MyProgramCore STATIC ProcessInvoker.c SystemManager.c) 

Después de una serie de comandos cmake . && make && sudo checkinstall cmake . && make && sudo checkinstall sistema de compilación CMake se completa con éxito. El primer comando comienza a procesar el archivo CMakeLists.txt en el directorio raíz del proyecto, el segundo comando finalmente compila los archivos binarios necesarios y el tercer comando instala el MyProgram compilado en el sistema.


Conclusión


Ahora puede escribir los suyos y comprender los archivos CMake de otras personas, y puede leer en detalle sobre otros mecanismos en el sitio web oficial .


El próximo artículo de esta guía se centrará en probar y crear paquetes con CMake y se lanzará en una semana. Hasta pronto!

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


All Articles