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
# 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
# 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
# Compila los archivos fuente especificados para generar un archivo ejecutable
add_executable(MyApp main.cpp utils.cpp)
Añadir una biblioteca:
Ejemplo
# 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
# 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
# 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
# [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
# [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
# 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
# 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
# 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
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
# 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
# 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
# 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
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.
Otras extensionesLos 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.