Fundamentos de CMake

CMake es una herramienta multiplataforma de generación de sistemas de construcción, utilizada para gestionar el proceso de compilación de proyectos C/C++.

Al escribir archivos de configuración CMakeLists.txt, puedes definir las reglas de construcción, dependencias y opciones de compilación del proyecto; CMake genera automáticamente los archivos de construcción correspondientes a la plataforma (como Makefile en Unix o soluciones de Visual Studio en Windows).


Archivo CMakeLists.txt

CMakeLists.txt es el archivo de configuración principal de CMake; cada proyecto CMake necesita al menos un archivo CMakeLists.txt.

CMake entiende la estructura del proyecto, la lista de archivos fuente y los requisitos de compilación leyendo las instrucciones del archivo.

Sintaxis básica

CMakeLists.txt está compuesto por una serie de directivas de CMake, cada directiva tiene el formato:nombre_del_comando(lista_de_argumentos)。

A continuación se muestran las directivas más comunes y sus ejemplos al construir un proyecto CMake.

Especificar la versión mínima de CMake:

Ejemplo

# Sintaxis: cmake_minimum_required(VERSION <version>)
# Esta directiva debe colocarse en la parte superior de CMakeLists.txt
# Ejemplo: se requiere CMake versión no inferior a 3.10
cmake_minimum_required(VERSION 3.10)

Definir el nombre del proyecto y el lenguaje:

Ejemplo

# Sintaxis: project(<nombre_del_proyecto> [<lenguaje>...])
# El parámetro de lenguaje es opcional, valores comunes: CXX (C++), C (lenguaje C)
# Al llamar a project(), se establecen automáticamente variables como PROJECT_NAME
project(MyProject CXX)

Añadir un ejecutable:

Ejemplo

# Sintaxis: add_executable(<objetivo> <archivos_fuente>...)
# Compila los archivos fuente especificados para generar un archivo ejecutable
add_executable(MyApp main.cpp utils.cpp)

Añadir una biblioteca:

Ejemplo

# Sintaxis: add_library(<objetivo> [STATIC | SHARED | MODULE] <archivos_fuente>...)
# STATIC: biblioteca estática (.a / .lib), se incrusta directamente en el ejecutable al compilar
# SHARED: biblioteca dinámica (.so / .dll), se carga en tiempo de ejecución
# Si no se especifica el tipo, lo decide la variable BUILD_SHARED_LIBS
add_library(MyLib STATIC library.cpp)

Enlazar bibliotecas al objetivo:

Ejemplo

# Sintaxis: target_link_libraries(<objetivo> <biblioteca>...)
# Enlaza la biblioteca especificada al objetivo (ejecutable u otra biblioteca)
# Se pueden enlazar objetivos de bibliotecas del propio proyecto o bibliotecas externas
target_link_libraries(MyApp PRIVATE MyLib)

Establecer variables:

Ejemplo

# Sintaxis: set(<nombre_de_variable> <valor>...)
# Define una variable normal, referenciada posteriormente con ${nombre_de_variable}
set(CMAKE_CXX_STANDARD 17)

# También se pueden establecer múltiples valores (lista)
set(SOURCES main.cpp utils.cpp helper.cpp)
add_executable(MyApp ${SOURCES})

Especificar la ruta de archivos de cabecera para el objetivo:

Ejemplo

# Sintaxis: target_include_directories(<objetivo>
#           [BEFORE | AFTER]
#           [SYSTEM]
#           [PUBLIC | PRIVATE | INTERFACE]
# <rutas>...)
# PUBLIC: tanto el objetivo actual como los que dependen de él pueden usar esta ruta
# PRIVATE: solo el objetivo actual puede usarla
# INTERFACE: solo otros objetivos que dependen del objetivo actual pueden usarla
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/include)

Establecer reglas de instalación:

Ejemplo

# Sintaxis: install(TARGETS <objetivo>...
# [RUNTIME DESTINATION <ruta_de_instalación_del_ejecutable>]
# [LIBRARY DESTINATION <ruta_de_instalación_de_biblioteca_dinámica>]
# [ARCHIVE DESTINATION <ruta_de_instalación_de_biblioteca_estática>]
# [INCLUDES DESTINATION <ruta_de_instalación_de_cabeceras>])
# Define la ubicación de instalación de cada tipo de archivo al ejecutar make install
install(TARGETS MyApp RUNTIME DESTINATION bin)

Sentencias condicionales:

Ejemplo

# Sintaxis: if(<condición>) ... elseif(<condición>) ... else() ... endif()
# Las expresiones condicionales admitidas por CMake incluyen: comparación, operaciones lógicas, comprobación de variables, etc.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    # Habilitar información de depuración en modo Debug
    message(STATUS "Modo de construcción Debug actual")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
else()
    # Habilitar optimizaciones en modo Release
    message(STATUS "Modo de construcción Release actual")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")
endif()

Comandos personalizados:

Ejemplo

# Sintaxis: add_custom_command(
# TARGET <objetivo>
#          PRE_BUILD | PRE_LINK | POST_BUILD
# COMMAND <comando> [argumentos...]
# [COMMENT <descripción>]
#          [VERBATIM])
# PRE_BUILD: se ejecuta antes de compilar
# PRE_LINK: se ejecuta antes de enlazar
# POST_BUILD: se ejecuta después de completar la construcción
add_custom_command(
    TARGET MyApp POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo "¡Construcción completada!"
    COMMENT "Mostrar mensaje de construcción completada"
)

Ejemplo completo

A continuación se muestra un ejemplo completo de CMakeLists.txt que utiliza varias de las directivas anteriores.

Ejemplo

# Ruta del archivo: CMakeLists.txt en la raíz del proyecto

# 1. Especificar la versión mínima de CMake (debe ir al principio)
cmake_minimum_required(VERSION 3.10)

# 2. Definir nombre, versión y lenguaje del proyecto
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

# 3. Establecer el estándar de C++ como C++17 y exigir su uso
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 4. Añadir un objetivo de biblioteca estática
add_library(MyLib STATIC
    src/lib/core.cpp      # Implementación de la funcionalidad principal
    src/lib/helper.cpp    # Implementación de funciones auxiliares
)

# 5. Añadir un objetivo de ejecutable
add_executable(MyApp
    src/main.cpp           # Punto de entrada del programa
    src/utils.cpp          # Funciones de utilidad
)

# 6. Especificar la ruta pública de cabeceras para el objetivo de biblioteca
# PUBLIC significa que los objetivos que usan MyLib también pueden acceder a esta ruta
target_include_directories(MyLib PUBLIC ${PROJECT_SOURCE_DIR}/include)

# 7. Especificar la ruta privada de cabeceras para el ejecutable
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/src)

# 8. Enlazar MyLib a MyApp
target_link_libraries(MyApp PRIVATE MyLib)

# 9. Establecer reglas de instalación
install(TARGETS MyApp RUNTIME DESTINATION bin)
install(TARGETS MyLib ARCHIVE DESTINATION lib)

Los comandos típicos para construir este proyecto son:

# 在项目根目录下创建 build 目录(推荐外部构建)
$ mkdir build && cd build

# 生成构建文件
$ cmake ..

# 编译项目
$ cmake --build .

# 安装到系统目录(可选)
$ cmake --install .

Variables y caché

CMake utiliza variables para almacenar y transmitir información de configuración; las variables se pueden definir y usar en CMakeLists.txt.

Las variables se dividen en dos categorías:Variables normalesyVariables de caché, sus alcances y ciclos de vida son diferentes.

Variables normales

Las variables normales se definen en CMakeLists.txt y su alcance es el directorio actual y sus subdirectorios.

Las variables normales se recalculan en cada ejecución de CMake y no se conservan.

Ejemplo

# Definir una variable de cadena
set(MY_APP_NAME "ejemplo-app")

# Definir una variable de lista (separada por punto y coma o espacios)
set(SOURCE_FILES main.cpp utils.cpp helper.cpp)

# Usar variables: referencia mediante ${nombre_variable}
message(STATUS "Nombre del proyecto: ${MY_APP_NAME}")

# Usar variables de lista: iterar sobre cada archivo fuente
foreach(FILE ${SOURCE_FILES})
    message(STATUS "Archivo fuente: ${FILE}")
endforeach()

Variables de caché

Las variables de caché se almacenan en el archivo CMakeCache.txt y conservan su valor entre múltiples ejecuciones de CMake.

Las variables de caché se usan normalmente para permitir que los usuarios personalicen la configuración de compilación en la fase de configuración de CMake, por ejemplo, activar o desactivar funciones o especificar la ruta de instalación.

El valor de las variables de caché es persistente. Si modificas el valor predeterminado de una variable de caché en CMakeLists.txt, necesitas eliminar CMakeCache.txt o volver a ejecutar cmake para que el nuevo valor predeterminado surta efecto.

Ejemplo

# Sintaxis: set(<nombre_variable> <valor_predeterminado> CACHE <tipo> <descripción>)
# Tipos disponibles: STRING, BOOL, PATH, FILEPATH
# Si la variable ya existe en CMakeCache.txt, set no sobrescribirá el valor existente

# Definir una variable de caché de tipo cadena (ruta de instalación modificable por el usuario)
set(MY_INSTALL_PATH "/usr/local" CACHE PATH "Ruta de instalación del programa")

# Definir una variable de caché de tipo booleano (interruptor de función)
set(ENABLE_LOGGING ON CACHE BOOL "Si habilitar la función de registro")

# Usar las variables de caché igual que otras variables
if(ENABLE_LOGGING)
    message(STATUS "Función de registro habilitada")
    add_definitions(-DENABLE_LOGGING)
endif()

Los usuarios pueden, mediante la línea de comandos,-Dmodificar las variables de caché con parámetros:

# 在 cmake 命令中通过 -D 修改缓存变量
$ cmake .. -DMY_INSTALL_PATH=/opt/ejemplo -DENABLE_LOGGING=OFF

Rutas de búsqueda de archivos de cabecera

En CMake, hay dos formas de especificar las rutas donde el compilador busca los archivos de cabecera:include_directories()ytarget_include_directories()。

Comprender la diferencia entre ambas es muy importante para escribir proyectos CMake claros y mantenibles.

Comparación entre include_directories y target_include_directories

Ambas pueden añadir rutas de búsqueda de archivos de cabecera para el compilador, pero difieren en el alcance y el nivel de control.

Característica include_directories() target_include_directories()
Alcance Ámbito global, afecta a todos los objetivos del directorio actual y sus subdirectorios Solo afecta al objetivo especificado, no contamina otros objetivos
Nivel de recomendación No recomendado, salvo para mantener proyectos CMake antiguos Recomendado como primera opción, conforme a las mejores prácticas del CMake moderno
Vinculación con objetivos No está vinculado directamente a un objetivo específico, puede afectar accidentalmente a otros objetivos Se vincula explícitamente al objetivo especificado, las dependencias quedan claras de un vistazo
Mantenibilidad Deficiente, fácilmente causa contaminación global de rutas y problemas difíciles de diagnosticar Buena, las rutas quedan vinculadas al objetivo y la lógica es clara
Control de visibilidad No puede controlar con precisión el alcance de propagación Control preciso mediante PUBLIC, PRIVATE e INTERFACE

Significado de PUBLIC / PRIVATE / INTERFACE

target_include_directories()Su ventaja clave es que permite controlar con precisión la propagación de las rutas de archivos de cabecera mediante las palabras clave de visibilidad.

Palabra clave Disponible para el objetivo actual Disponible para otros objetivos que dependen de este objetivo Escenario típico
PRIVATE Sí no Archivos de cabecera internos necesarios solo para la propia implementación
PUBLIC Sí Sí Archivos de cabecera de la API pública de la biblioteca, necesarios tanto para la propia biblioteca como para los consumidores
INTERFACE no Sí Biblioteca de solo cabeceras (header-only library)

Ejemplo

# Escenario: los archivos de cabecera de MyLib están en el directorio include/
# MyLib necesita include/ por sí misma, y MyApp, que usa MyLib, también necesita include/

# PUBLIC: tanto la propia biblioteca como los consumidores pueden acceder a esa ruta
target_include_directories(MyLib PUBLIC ${PROJECT_SOURCE_DIR}/include)

# PRIVATE: solo MyApp puede acceder (archivos de cabecera de funciones de utilidad internas)
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/src/internal)

# INTERFACE: MyLib es una biblioteca de solo cabeceras, no necesita compilarse, solo transmite la ruta a los consumidores
add_library(HeaderOnlyLib INTERFACE)
target_include_directories(HeaderOnlyLib INTERFACE ${PROJECT_SOURCE_DIR}/header_only)

Buscar bibliotecas y paquetes

Muchos proyectos dependen de bibliotecas externas (como Boost, OpenCV). CMake proporcionafind_package()directivas para detectar y configurar automáticamente estas dependencias externas.

Directiva find_package

find_package()busca bibliotecas instaladas en el sistema y establece las variables correspondientes (como rutas de archivos de cabecera, rutas de archivos de biblioteca y nombres de objetivos de biblioteca).

Ejemplo

# Uso básico: buscar la biblioteca Boost (sin restringir la versión)
# REQUIRED indica que, si no se encuentra, se aborta la configuración de CMake
find_package(Boost REQUIRED)

# Especificar la versión mínima: exige que la versión de Boost no sea inferior a 1.70
find_package(Boost 1.70 REQUIRED)

# Especificar la ruta de búsqueda: buscar OpenCV en una ruta personalizada
find_package(OpenCV REQUIRED PATHS /path/to/opencv)

Usar la biblioteca encontrada

find_package()Tras el éxito, normalmente se proporciona lo siguiente para su uso:

Contenido proporcionado Ejemplo Descripción
Objetivo importado Boost::Boost Forma recomendada por el CMake moderno, se usa directamente mediante target_link_libraries
Variable de ruta de archivos de cabecera ${Boost_INCLUDE_DIRS} Uso de estilo antiguo, se usa junto con include_directories
Variable de ruta de archivos de biblioteca ${Boost_LIBRARY_DIRS} Uso de estilo antiguo, se usa junto con link_directories

Se recomienda encarecidamente usarobjetivos importados(como Boost::Boost), en lugar de concatenar manualmente rutas de archivos de cabecera y de bibliotecas. Los objetivos importados gestionan automáticamente todas las opciones de compilación y enlazado, lo que reduce los errores de configuración.

Ejemplo de uso de una biblioteca de terceros

A continuación se muestra un ejemplo completo de cómo incorporar y usar la biblioteca Boost en un proyecto CMake.

Ejemplo

# Ruta del archivo: raíz_del_proyecto/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyBoostProject CXX)

# Establecer el estándar de C++
set(CMAKE_CXX_STANDARD 17)

# Buscar la biblioteca Boost; si no se encuentra, se aborta con un error
find_package(Boost REQUIRED)

# Añadir el ejecutable
add_executable(MyApp main.cpp)

# Enlazar Boost usando el objetivo importado
# Boost::Boost establece automáticamente las rutas de archivos de cabecera y de bibliotecas
target_link_libraries(MyApp PRIVATE Boost::Boost)

Notas

Prioriza el uso de las directivas de la serie target_xxx.include_directories() y link_directories() son directivas globales que fácilmente causan contaminación de rutas. En proyectos grandes, se deben usar siempre target_include_directories() y target_link_libraries(), ya que hacen que las dependencias sean más claras y fáciles de mantener.

Especifica siempre el estándar de C++.Si no se establece CMAKE_CXX_STANDARD, el compilador usará el estándar predeterminado (normalmente C++98 o C++14, según la versión del compilador). Se recomienda establecer también CMAKE_CXX_STANDARD_REQUIRED en ON para garantizar que el compilador muestre un error directamente si no admite el estándar especificado.

Comprende la diferencia entre PUBLIC / PRIVATE / INTERFACE.Usar incorrectamente estas palabras clave es el error más común entre los principiantes de CMake. Para recordarlo fácilmente: lo necesitan tanto la propia biblioteca como los consumidores → PUBLIC; solo lo necesita la propia biblioteca → PRIVATE; solo lo necesitan los consumidores (como una biblioteca de solo cabeceras) → INTERFACE.

Los valores predeterminados de las variables de caché no se actualizan automáticamente.Después de modificar el valor predeterminado de set(... CACHE ...) en CMakeLists.txt, el CMakeCache.txt existente no se sobrescribirá. Es necesario eliminar el archivo CMakeCache.txt del directorio build, o todo el directorio build, y volver a ejecutar cmake.

Otras extensiones