Algoritmos y Estructuras de Datos Herramientas Lenguaje de programación
!Prog C/C++ Rust
Linux Matemáticas
Mates Discretas
Programación Orientada a Objetos Sistemas Operativos

CMake

[date: 05-08-2021 00:00] [last modification: 19-07-2023 17:40]
[words: 2611] [reading time: 13min] [size: 85006 bytes]

CMake es una de las build tools más utilizadas para proyectos C++.

Introducción

Make es un build system , es decir, un programa que se utiliza para compilar otros programas y hacer así la vida del desarrollador más sencilla. CMake , sin embargo, genera automáticamente estos programas Make con la ventaja de no solamente poder exportarlo a Make , si no a muchos otros build system y haciendo que sea fácil de compilar en diferentes plataformas.

Todo comienza con un archivo sencillo llamado CMakeLists.txt. En este archivo se podrá escribir toda la configuración necesaria. Este archivo debe estar situado en la base del proyecto para que este pueda acceder fácilmente al source.

CMake es Case Sensitive (distingue mayúsculas de minúsculas), pero no le importan los espacios en blanco. Sin embargo, en algunos casos se permite usar tanto mayúsculas como minúsculas para hacer llamadas a las funciones de CMake. Esto se debe a que anteriormente se hacía todo en mayúsculas. Actualmente, por convención se suelen escribir los nombres de las variables en mayúsculas y las funciones en minúsculas. Los strings entre comillas dobles (").

CMakeLists.txt básico

Lo que todo proyecto debe de tener (por convención principalmente porque igualmente funcionará si no lo incluimos) es una versión mínima de CMake :

1cmake_minimum_required(VERSION <X.X.X> [FATAL_ERROR])

Después de añadir esto, podemos declarar nuestro proyecto:

1project(
2      <nombre del proyecto>
3      [VERSION <versión>]
4      [DESCRIPTION <descripción>]
5      [LANGUAGES <lenguajes>]
6      [HOMEPAGE_URL <url>]
7)

Y para comenzar con un ejemplo básico, comenzaremos por un ejecutable sencillo:

1add_executable(<ejecutable> [<sources>])

Compilación con CMake

El proceso de compilación es bastante sencillo, en primer lugar es recomendable crear una carpeta específica para guardar los archivos generados automáticamente por CMake. Normalmente, a esta carpeta se la llama build.

1mkdir build

Ahora, podemos realizar la misma operación (la configuración de CMake o la generación del Makefile ) de diferentes formas, usando cualquiera de estos comandos:

1cmake ..            # Desde build
2cmake -S . -B build # Desde la raíz
3
4# El comando en general es:
5cmake -S <dirección_del_CMakeLists> -B <dirección_donde_compilar>

En algunos casos, da un error porque no conoce o no tenemos instalado el Make por defecto que genera CMake. Para cambiarlo, usamos la bandera -G "<tipo_de_Makefile>". Para saber cuales tienes disponibles simplemente escribe el comando cmake -G. Recuerda que si te ha saltado un error de este tipo y luego cambias tu tipo de Makefile no funcionará, porque no se sobrescribirá. Deberás borrar todos los archivos generados y repetir el comando.

Una vez generado el Makefile, podemos utilizarlo como de costumbre o llamar a CMake para que lo haga por nosotros:

1cmake --build build

Sintaxis básica

Comentarios

Podemos hacer comentarios para clarificar nuestro código CMake. Creo que solo pueden ser de una línea:

1# Esto es un comentario

Variables

Las variables en CMake funcionan igual que en cualquier otro lenguaje de programación: no pueden haber varias con el mismo nombre, tienen un alcance…

Para obtener el valor de la variable, se escribe entre llaves con un dolar delante: ${<nombre_variable>}.

1set(<nombre> <valores> [PARENT_SCOPE])

Además de esto, podemos añadirle lo siguiente para que el valor se guarde en caché y pueda usarse entre varios CMakeLists.txt, como una variable global.

1set(<nombre> <valores> CACHE <TIPO> "<descripción>" [FORCE])

Como punto final, también se pueden establecer variables del entorno desde CMake.

1set(ENV{<nombre>} [<valor>])

Si el valor está vacío, se borra la variable del entorno.

Tipos de datos de la variables
BOOLON/OFF
FILEPATHDirección a un archivo. En CMake GUI se muestra un explorador de archivos.
PATHigual que el anterior pero la dirección es a una carpeta.
STRINGGuarda texto.
INTERNALIgual que el anterior, pero esta es la que se utiliza para guardar datos entre varios CMakeLists.txt. Además, no aparece en CMake GUI.

Algunas variables que nos ofrece CMake

En la documentación de las variables puedes encontrar una lista exhaustiva de todas las variables. Estas son probablemente las más útiles.

Paths útiles
CMAKE_SOURCE_DIRPath a la carpeta base de source
CMAKE_CURRENT_SOURCE_DIRPath del CMakeLists.txt actual
PROJECT_SOURCE_DIRsource del proyecto actual
CMAKE_BINARY_DIRPath a donde compilas
CMAKE_CURRENT_BINARY_DIRPath actual de la compilación
PROJECT_BINARY_DIRPath de compilación para el proyecto actual
Compilador y flags
CMAKE_C_COMPILER y CMAKE_CXX_COMPILERCambia el compilador por defecto
CMAKE_BUILD_TYPETipo de ejecutable generado: Debug, Release, RelWithDebInfo, MinSizeRel
CMAKE_CXX_FLAGS_DEBUGFlags para el compilador en configuración Debug
CMAKE_CXX_FLAGS_RELEASEFlags para el compilador en la configuración Release
CMAKE_CXX_FLAGS_RELWITHDEBINFOFlags para el compilador en la configuración RelWithDebInfo
CMAKE_CXX_FLAGS_MINSIZERELFlags para el compilador en la configuración MinSizeRel

Operaciones con variables

Con el siguiente método podemos hacer operaciones matemáticas con variables:

1math(EXPR <output_var> <mat_expresión> [HEXADECIMAL | DECIMAL])

Opciones

Existen unas «variables» booleanas (ON/OFF) especiales que podemos declarar, llamadas opciones. Estas son especiales porque el usuario puede modificarlas desde la línea de comandos o CMake GUI.

1option(<nombre> "<descripción>" [<valor>])

Si no damos ningún valor, por defecto estará desactivada (OFF). El usuario puede cambiar su valor usando la flag -D<nombre_variable>=<valor>, o bien sobreescribiéndola:

1set(<nombre> <valor> CACHE BOOL "" FORCE)

Listas

CMake docs

Estructuras de control

Condicionales

En muchos casos, es necesario tomar decisiones: si compilar esto, si unirlo con lo otro… Para ello tenemos la siguiente estructura:

1if(<condición>)
2    <código>
3elseif(<condición>) # Opcional, puede ser repetido
4    <código>
5else() # Opcional
6    <código>
7endif()

Disponemos de los siguientes operadores para realizar las condiciones (además de contar con los típicos operadores lógicos AND, OR y NOT):

Comprueban si existen determinados elementos:

Más información: <cmake.org/cmake/help/…>

Bucles

En algunos casos queremos repetir ciertas operaciones, para ello podemos usar estos bucles:

1foreach(<elemento> <items>)
2    <código>
3endforeach()
4
5foreach(<elemento> RANGE [<start>] <end> [<step>])
6foreach(<elemento> IN LIST <lista>)

Además de eso, podemos usar break() y continue() para controlar mejor el flujo.

Macros

Trabajar con proyectos grandes

En algunos casos, nos encontraremos entre cientos y cientos de archivos para un mismo programa, y para manejarlos todos desde solamente un CMakeLists.txt, este se nos queda pequeño.

Para evitar eso, podemos declarar más de estos archivos y incluirlos a nuestro CMakeLists.txt principal de la siguiente forma siempre y cuando estos se distribuyan en subdirectorios:

1add_subdirectory(<path> [<build_path>])

Básicamente lo que hace es añadir un subdirectorio y ejecuta cualquier CMakeLists.txt de este. Los targets declarados ahí se conservan, pero las variables convencionales no. Recuerda guardarlas en caché si las necesitas.

Sin embargo, en algunos casos, no tenemos subdirectorios. En ese caso podemos usar:

1include(<path> [OPTIONAL][RESULT_VARIABLE <var>])

Esta instrucción funciona igual que los #include de C/C++, simplemente añaden código al CMakeLists.txt. Es útil cuando tienes partes comunes entre varios CMakeLists pero solo quieres escribir el código una vez.

Mostrar información

Mostrar el comando de compilación

En muchos casos, ayuda para depurar qué comandos se están ejecutando para compilar. Esto se puede hacer con la opción --verbose.

1cmake --build build --verbose

Mostrar mensajes

Para mostrar información al usuario, al desarrollador, etc, podemos usar lo siguiente:

1message([OPCIONES] <mensaje>)

Configuración

Cambiar el compilador

Por lo general, no nos suele importar qué compilador estamos usando; pero en algunos casos hace falta. Para configurar eso, existen las variables CMAKE_C_COMPILER y CMAKE_CXX_COMPILER.

No se recomienda fijarlas directamente en el archivo CMakeLists.txt con set (en caso de que fuese necesario, es mejor hacerlo antes de la llamada a project), sino pasarlas por línea de comandos:

1cmake . -B build -DCMAKE_C_COMPILER=<compiler>

Alternativamente, puedes usar las variables de entorno CC para el compilador de C y CXX para el compilador de C++.

Tipo de ejecutable

En algunos casos, queremos configurar cómo será nuestro ejecutable: si debe de guardar los símbolos necesarios para usar un debugger , cuanto de optimizado debe estar, etc.

Para esto, CMake nos ofrece la posibilidad de usar la variable CMAKE_BUILD_TYPE. Los valores más típicos son los siguientes, aunque también es posible añadir otros personalizados (como por ejemplo Distribution).

TipoCaracterísticasFlagsValor por defecto
DebugSin optimizaciones y con información de depuraciónCMAKE_<lang>_FLAGS_DEBUG-g
ReleaseOptimizado y sin assertsCMAKE_<lang>_FLAGS_RELEASE-O3 -DNDEBUG
RelWithDebInfoOptimizado pero con información de depuraciónCMAKE_<lang>_FLAGS_RELWITHDEBINFO-O2 -g -DNDEBUG
MinSizeRelOptimizado por tamaño del ejecutableCMAKE_<lang>_FLAGS_MINSIZEREL-Os -DNDEBUG

Donde <lang> es C o CXX.

Escoger un estándar

1set(CMAKE_CXX_STANDARD 11)
2set(CMAKE_CXX_STANDARD_REQUIRED True)

Targets

Los targets son el propósito de CMake en general: los ejecutables, librerías, binarios… Estos se crean con add_executable() y add_library():

1add_executable(<target> [<sources>])
2add_library(<target> <SHARED | STATIC | MODULE> [<sources>])

En ambos casos los sources son opcionales porque podemos añadirlos de la siguiente forma:

1target_sources(<target> <PUBLIC | PRIVATE | INTERFACE> <sources>)

Un truco para cargar muchos sources

Cuando tenemos muchos archivos fuente, puede que no sea buena idea escribirlos todos a mano en CMake. Lo que podemos hacer, en cambio, es, si los tenemos ordenados en sus respectivas carpetas, podemos buscarlos por su extensión:

1file(GLOB <output_var> <extensiones>)
2file(GLOB_RECURSE <output_var> <extensiones>) # Lo mismo, pero recursivo

Más información sobre file.

Alias

Da a la librería un ALIAS para ser utilizado en contextos read-only:

1add_library(<alias_name> ALIAS <target>) # Por convención: alias::name

Añadir los includes a un target

Este es el equivalente a hacer -I en el compilador:

1target_include_directories(<target> <PUBLIC | PRIVATE | INTERFACE> <include_paths>)

Enlazar librerías a targets

Este es el equivalente a hacer -l en el compilador:

1target_link_libraries(<target> <librerías>)

Las librerías que incluyamos aquí ha debido ser creada por add_library() o ser importada.

Crear una librería estática y dinámica a la vez

Para evitar compilar de nuevo la librería, en primer lugar se crea una común marcada como OBJECT y luego esta se enlazan para crear la librería estática y dinámica.

1add_library(<main_target>   OBJECT <sources>)
2add_library(<static_target> STATIC)
3add_library(<shared_target> SHARED)
4
5target_link_libraries(<static_target> PUBLIC <main_target>)
6target_link_libraries(<shared_target> PUBLIC <main_target>)

Importar y exportar targets

Para importar librerías y ejecutables ya creados como targets (es decir, que no necesitan compilación por parte de CMake) se usa la opción IMPORTED.

1add_executable(<target> IMPORTED)
2add_library(<target> <SHARED | STATIC | MODULE> IMPORTED)

Y para decirle a CMake donde está la librería / el ejecutable se debe usar set_property() de la siguiente forma:

1set_property(TARGET <target> PROPERTY IMPORTED_LOCATION "<path>/<file>")

Además, si se da el caso, es posible importar distintas configuraciones de la mismo target:

1add_library(math STATIC IMPORTED GLOBAL)
2set_target_properties(math PROPERTIES
3  IMPORTED_LOCATION "${math_REL}"
4  IMPORTED_LOCATION_DEBUG "${math_DBG}"
5  IMPORTED_CONFIGURATIONS "RELEASE;DEBUG"
6)

Instalar

CMake a código

CMake nos ofrece la posibilidad de modificar nuestro código C++. Esto puede ser útil para tener en cuenta la versión del código y otros aspectos.

1configure_file(<input_file> <output_file> [COPYONLY][@ONLY])

Básicamente, copia el archivo de entrada (normalmente de extensión .in) en uno de salida sustituyendo el nombre de las variables (@<var>@ o ${<var>}) por su valor en el archivo de entrada. Recuerda añadir el archivo resultante al includepath (se genera en ${CMAKE_CURRENT_BINARY_DIR})

COPYONLY: solo copia el contenido de un archivo al otro, no modifica su interior

@ONLY: solo modifica aquellas variables entre @<var>@ porque en algunos casos puede haber incompatibilidades.

Además, las líneas como #cmakedefine <var> ... se remplazarán con #define <var> ... o /* #undef <var> */ dependiendo de como actúa el valor de <var> en un if.

Como punto final, si añadimos #cmakedefine01 <var>, se sustituirá por #define <var> 0 o #define <var> 1

<cmake.org/cmake/help/…>

Librerías

Para incluir librerías en nuestro proyecto, CMake nos ofrece varias formas de abordar esta situación:

 1find_library(<var> <name> [<paths>])
 2find_library(
 3    <var>
 4    <name>|NAMES <names>
 5    [HINTS <paths>]
 6    [PATHS <paths>]
 7    [PATH_SUFFIXES <path_suffixes>]
 8    [DOC <doc>]
 9    [REQUIRED]
10    [OTROS]
11)
 1find_package(
 2    <name>
 3    [NAMES <names>]
 4    [<version>]
 5    [EXACT]
 6    [QUIET]
 7    [REQUIRED]
 8    [COMPONENTS <componentes>]
 9    [OPTIONAL_COMPONENTS <componentes_opcionales>]
10    [MODULE]
11    [CONFIG|NO_MODULE]
12    [CONFIGS <configs>]
13    [HINTS <paths>]
14    [PATHS <paths>]
15    [PATH_SUFFIXES <path_suffixes>]
16    [OTROS]
17)

Busca y carga la configuración de un proyecto externo. <name>_FOUND si se encuentra el paquete.

En el modo MODULE, CMake busca por un archivo llamado Find<name>.cmake. Primero se busca en las direcciones de CMAKE_MODULE_PATH, después entre los find modules dados por la instalación de CMake. Si se encuentra este archivo, se ejecuta: es responsable de encontrar el paquete, comprobar la versión y mostrar mensajes acordes.

Si no se especifica la opción MODULE, CMake primero busca usando este modo igualmente. Después, si no ha encontrado nada, continua otra vez usando el modo CONFIG. El modo MODULE principalmente se usa cuando la librería no utiliza CMake, por lo que tenemos que escribir el Find<name>.cmake por nosotros mismos y sopesar las diferentes posibilidades. El modo CONFIG es el que deberíamos utilizar cuando la librería sí soporta CMake.

Este modo (o también llamado NO_MODULE) intenta encontrar el archivo de configuración, llamado <name>Config.cmake o <lowercase_name>-config.cmake (por cada nombre dado). Una vez que se encuentra el archivo, se ejecuta y se crea una variable en caché llamada <name>_DIR que guarda la dirección de la carpeta del archivo, y <name>_CONFIG la dirección completa al archivo. CONFIGS son los nombres adicionales del paquete-config.cmake

Un paquete de config consiste en un archivo de configuración, y opcionalmente en un package version file.

Estas son las direcciones donde CMake busca estas librerías dependiendo de la plataforma en la que se esté. La forma más fácil de decirle otra dirección no-estándar es:

 1set(CMAKE_PREFIX_PATH “<path>” CACHE PATH “” FORCE)
 2
 3# W: Windows, U: Unix, A: Apple
 4# En todos los casos, el nombre es caseinsensitive
 5
 6(W) <prefix>/ (W) <prefix>/(cmake|CMake)/ (W) <prefix>/<name>*/ (W)
 7<prefix>/<name>*/(cmake|CMake)/ (W/U)
 8<prefix>/<name>*/(lib/<arch>|lib*|share)/cmake/<name>*/ (W/U)
 9<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/ (W/U)
10<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/ (U)
11<prefix>/(lib/<arch>|lib*|share)/cmake/<name>*/ (U)
12<prefix>/(lib/<arch>|lib*|share)/<name>*/ (U)
13<prefix>/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/ (A)
14<prefix>/<name>.framework/Resources/ (A)
15<prefix>/<name>.framework/Resources/Cmake/ (A)
16<prefix>/<name>.framework/Versions/*/Resources/ (A)
17<prefix>/<name>.framework/Versions/*/Resources/Cmake/ (A)
18<prefix>/<name>.app/Contents/Resources/ (A)
19<prefix>/<name>.app/Contents/Resources/Cmake/

Git submodules

 1# Download all the submodules
 2
 3find_package(Git QUIET)
 4
 5if (GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") # Update submodules as needed
 6    option(GIT_SUBMODULE "Check submodules during build" ON)
 7
 8    if (GIT_SUBMODULE)
 9        message(STATUS "Submodule update")
10        execute_process(
11            COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
12            WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
13            RESULT_VARIABLE
14            GIT_SUBMOD_RESULT
15        )
16
17        if (NOT GIT_SUBMOD_RESULT EQUAL "0")
18            message(FATAL_ERROR
19                "git submodule update --init -- recursive failed with ${GIT_SUBMOD_RESULT}, please checkout submodules")
20        endif()
21    endif()
22endif()
23
24# Check all submodules
25if (NOT EXISTS "${PROJECT_SOURCE_DIR}/...")
26    message(FATAL_ERROR "The ... submodule was not downloaded! GIT_SUBMODULE was turned off or failed. Please update submodules")
27endif()

Más (TODO)

Anterior: Programación Estructurada en C Volver a C/C++