Características avanzadas de CMake

Después de dominar los conceptos básicos de CMake, las características avanzadas pueden ayudarte a gestionar de manera más flexible la configuración de compilación de proyectos complejos.

Este artículo cubre seis temas principales: módulos personalizados, compilación de múltiples configuraciones, búsqueda avanzada de paquetes, pasos de compilación personalizados, compilación cruzada multiplataforma y configuración de propiedades de objetivos.

Resumen general del contenido:

Tema Capacidad central Escenarios de aplicación típicos
Módulos y scripts personalizados Encapsular lógica de CMake reutilizable Unificar opciones de compilador, lógica de búsqueda personalizada, reglas de compilación compartidas por el equipo
Configuración de compilación y objetivos Gestionar múltiples configuraciones y múltiples objetivos Cambio Debug/Release, compilación de múltiples productos de salida
Búsqueda y configuración avanzadas Localizar dependencias externas de forma flexible Buscar componentes específicos, transmitir información de compilación mediante archivos de configuración
Pasos de compilación personalizados Ampliar el flujo de compilación Generación de código, procesamiento de recursos, automatización posterior a la compilación
Multiplataforma y compilación cruzada Compilación orientada a múltiples plataformas Desarrollo embebido, compilación para diferentes sistemas operativos
Propiedades y configuración de objetivos Control preciso de compilación y enlace Configurar niveles de advertencia y rutas de enlace para objetivos específicos

Módulos y scripts de CMake personalizados

Cuando se necesita repetir la misma lógica en múltiples CMakeLists.txt, encapsular esa lógica en módulos personalizados puede reducir en gran medida la redundancia de código.

La esencia de los módulos y scripts personalizados es extraer funciones, macros y configuraciones comunes de CMake a archivos independientes para reutilizarlos en múltiples proyectos.

Módulos de CMake personalizados

Un módulo personalizado es un archivo con.cmakecomo extensión, que define funciones y macros de CMake que pueden cargarse con include.

Pasos de creación:

Paso Operación Descripción
1 Crearcmake/Directorio Crear en el directorio raíz del proyecto para almacenar de forma centralizada los módulos personalizados.
2 Crear el archivo de módulo Por ejemplo,cmake/MyModule.cmakeescribir funciones personalizadas
3 Ampliar la ruta de búsqueda de módulos Registrar ese directorio en CMakeLists.txt mediante CMAKE_MODULE_PATH.
4 Cargar el módulo Usar include() para importar el módulo y llamar a las funciones definidas en él.

Ejemplo de módulo personalizado: MyModule.cmake

Ejemplo

# Ruta del archivo: cmake/MyModule.cmake
# Módulo personalizado: proporciona funciones auxiliares comunes del proyecto

# Función: establece opciones de advertencia del compilador de manera uniforme para todos los objetivos
# Parámetro ARG_TARGET: nombre del objetivo que necesita configurar advertencias
function(ejemplo_set_warnings ARG_TARGET)
    # Establece diferentes banderas de advertencia según el tipo de compilador
    if(MSVC)
        # Compilador MSVC: habilita el nivel de advertencia /W4
        target_compile_options(${ARG_TARGET} PRIVATE /W4)
    else()
        # Compiladores GCC / Clang: habilitan advertencias comunes
        target_compile_options(${ARG_TARGET} PRIVATE -Wall -Wextra -Wpedantic)
    endif()
endfunction()

# Función: imprime la información básica de compilación del objetivo
function(ejemplo_print_target_info ARG_TARGET)
    message(STATUS "Nombre del objetivo: ${ARG_TARGET}")
    message(STATUS "Lista de archivos fuente: $<TARGET_PROPERTY:${ARG_TARGET},SOURCES>")
endfunction()

Cargar y usar el módulo en CMakeLists.txt:

Ejemplo

# Ruta del archivo: CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject CXX)

# Agrega el directorio cmake/ a la ruta de búsqueda de módulos
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")

# Carga el módulo personalizado
include(MyModule)

# Agrega un objetivo de ejecutable
add_executable(MyApp main.cpp)

# Llama a la función personalizada del módulo para configurar las opciones de advertencia de MyApp
ejemplo_set_warnings(MyApp)

# Llama a la función personalizada del módulo para imprimir la información del objetivo
ejemplo_print_target_info(MyApp)

CMAKE_MODULE_PATHEs la lista de rutas de búsqueda que usa CMake para localizar archivos de módulos. CMake primero busca en las rutas de módulos integradas y, si no las encuentra, busca en las rutas especificadas por CMAKE_MODULE_PATH.

Uso de scripts de CMake personalizados

Los scripts personalizados son similares a los módulos, pero suelen usarse para ejecutar operaciones de configuración en lugar de definir funciones reutilizables.

Los scripts pueden cargarse directamente con include() y CMake ejecutará cada instrucción en orden.

Crear el archivo de script config.cmake:

Ejemplo

# Ruta del archivo: config.cmake
# Script personalizado: gestiona de forma centralizada las opciones de configuración del proyecto

# Establece el tipo de compilación predeterminado en Release
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Tipo de compilación" FORCE)
    message(STATUS "No se especificó el tipo de compilación, se usará Release por defecto")
endif()

# Configura diferentes opciones de compilación según el tipo de compilación
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    message(STATUS "Habilitar modo Debug: desactivar la optimización, activar los símbolos de depuración")
else()
    message(STATUS "Habilitar modo Release: activar la optimización O2")
endif()

Llamar al script en CMakeLists.txt:

# 使用绝对路径加载脚本
include(${CMAKE_SOURCE_DIR}/config.cmake)

Configuración de compilación y objetivos

CMake permite gestionar múltiples configuraciones de compilación (como Debug y Release) en el mismo proyecto y definir múltiples objetivos de compilación.

Generadores de múltiples configuraciones

Los diferentes generadores de CMake manejan la configuración de manera distinta.

Al igual que Visual Studio y Xcode,los generadores de múltiples configuracionespermiten cambiar entre Debug/Release en el mismo directorio de compilación, mientras que Unix Makefiles y otrosgeneradores de configuración únicanecesitan determinar el tipo de compilación en la fase de configuración.

Tipo de generador Generador típico Forma de especificar la configuración
Configuración única Unix Makefiles、Ninja En la configuración, mediante-DCMAKE_BUILD_TYPE=Releasese especifica
Múltiples configuraciones Visual Studio、Xcode En la compilación, mediante--config Releasese cambia

Establecer la configuración predeterminada en CMakeLists.txt:

# 仅对单配置生成器生效,多配置生成器会忽略此变量
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type")

CMAKE_BUILD_TYPE solo es válido para generadores de configuración única. Si usas Visual Studio, cambia la configuración mediante cmake --build . --config Release o desde el IDE.

Objetivos de compilación

En un proyecto CMake se pueden definir múltiples objetivos de compilación, y cada objetivo configura de forma independiente sus opciones de compilación y dependencias de enlace.

Esto te permite generar múltiples ejecutables o múltiples bibliotecas en una sola compilación.

Ejemplo

# Define dos objetivos ejecutables, cada uno con diferentes archivos fuente
add_executable(MyApp src/main.cpp)
add_executable(MyTool src/tool.cpp)

# Configura diferentes macros de compilación para diferentes objetivos, controlando la compilación condicional
set_target_properties(MyApp PROPERTIES COMPILE_DEFINITIONS "APP_MODE")
set_target_properties(MyTool PROPERTIES COMPILE_DEFINITIONS "TOOL_MODE")

# Enlaza diferentes bibliotecas para diferentes objetivos
target_link_libraries(MyApp PRIVATE MyLib)
target_link_libraries(MyTool PRIVATE MyLib CLI11::CLI11)

Búsqueda y configuración avanzadas

Además del uso básico de find_package, CMake también admite especificar componentes, controlar con precisión las versiones y transmitir información de compilación mediante plantillas de archivos de configuración.

Uso avanzado de find_package

COMPONENTSLa palabra clave te permite buscar solo submódulos específicos de la biblioteca, en lugar de todo el paquete.

Esto es muy importante para bibliotecas grandes como Boost: importar solo lo necesario evita enlazar componentes que no se necesitan.

Ejemplo

# Busca solo los componentes filesystem y system de Boost (no todo Boost)
# filesystem depende internamente de system, por lo que ambos deben declararse
find_package(Boost REQUIRED COMPONENTS filesystem system)

# Comprobar si se encontraron los componentes requeridos
if(Boost_FOUND)
    message(STATUS "Versión de Boost: ${Boost_VERSION}")
    message(STATUS "Ruta de archivos de cabecera de Boost: ${Boost_INCLUDE_DIRS}")

    # Enlazar cada componente necesario por separado
    target_link_libraries(MyApp PRIVATE
        Boost::filesystem
        Boost::system
    )
endif()

Especificar rutas de búsqueda (para ubicaciones de instalación no estándar):

# 方式一:通过变量指定根目录
cmake .. -DBOOST_ROOT=/path/to/custom/boost

# 方式二:在 CMakeLists.txt 中预先设置
set(BOOST_ROOT "/path/to/custom/boost")
find_package(Boost REQUIRED COMPONENTS filesystem)

Archivos de configuración y opciones de compilación

configure_file()La directiva puede inyectar valores de variables de CMake en archivos de código, logrando pasar información de configuración en tiempo de compilación.

Esto es muy útil cuando se necesita compilar información como el número de versión, la hora de compilación, etc., dentro del programa.

Crear el archivo de plantilla de configuración config.h.in:

Ejemplo

// Ruta del archivo: config.h.in
// Archivo de plantilla de configuración de CMake, que genera config.h mediante configure_file()

// Número de versión del proyecto (reemplazado por la variable de CMake @PROJECT_VERSION@)
#define EJEMPLO_VERSION "@PROJECT_VERSION@"

// Tipo de compilación (Debug o Release)
#define EJEMPLO_BUILD_TYPE "@CMAKE_BUILD_TYPE@"

// Marca de tiempo de compilación
#define EJEMPLO_BUILD_TIMESTAMP "@BUILD_TIMESTAMP@"

Configurar y generar el archivo de cabecera en CMakeLists.txt:

Ejemplo

# Obtener la marca de tiempo actual y guardarla en una variable
string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%d %H:%M:%S")

# Reemplazar @variables@ en config.h.in con los valores de las variables de CMake
# Salida a config.h en el directorio build
configure_file(config.h.in config.h)

# Agregar el directorio build a la ruta de búsqueda de archivos de cabecera para que los archivos fuente puedan encontrar config.h
include_directories(${CMAKE_BINARY_DIR})

Usar la configuración generada en el archivo fuente:

// 在 main.cpp 中包含生成的配置文件
#include "config.h"

std::cout << "EJEMPLO Version: " << EJEMPLO_VERSION << std::endl;
std::cout << "Build Type: " << EJEMPLO_BUILD_TYPE << std::endl;

Generar pasos de compilación personalizados

Además de la compilación y el enlazado estándar, CMake te permite insertar operaciones personalizadas en el proceso de construcción, como generación de código, copia de recursos y notificaciones posteriores a la construcción.

Comandos personalizados

add_custom_command()Se utiliza para definir un comando personalizado que se ejecuta cuando un archivo necesita actualizarse.

El concepto central de esta directiva esel impulso por dependencias— solo cuando el archivo OUTPUT no existe o los archivos DEPENDS se actualizan, el comando se ejecuta.

Ejemplo

# Comando personalizado: generar código a partir de un archivo de plantilla
# Se ejecuta solo si generated_file.cpp no existe o input_template.txt se actualiza
add_custom_command(
    # Archivo de salida (CMake decide si volver a ejecutar el comando verificando este archivo)
    OUTPUT ${CMAKE_BINARY_DIR}/generated_file.cpp

    # Comando a ejecutar (aquí se usa cmake -E para la operación integrada de generación de archivos)
    COMMAND ${CMAKE_COMMAND}
        -E echo "// Archivo fuente generado automáticamente" > ${CMAKE_BINARY_DIR}/generated_file.cpp

    # Archivo de dependencia (el comando se vuelve a ejecutar cuando este archivo cambia)
    DEPENDS ${CMAKE_SOURCE_DIR}/input_template.txt

    # Mensaje informativo mostrado durante la ejecución
    COMMENT "Generando archivo fuente desde la plantilla..."
)

Comando disparado en la fase de construcción—se ejecuta después del enlazado:

Ejemplo

# Después de que MyApp se construya, copiar el ejecutable al directorio de despliegue
add_custom_command(
    TARGET MyApp POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy
        $<TARGET_FILE:MyApp>
        ${CMAKE_SOURCE_DIR}/deploy/
    COMMENT "Copiando MyApp al directorio de despliegue"
)

Objetivos personalizados

add_custom_target()Define un objetivo de construcción que no produce archivos de salida, normalmente utilizado para agrupar varios comandos personalizados.

A menos que se use la palabra clave ALL, el objetivo personalizado no se ejecuta en la construcción predeterminada; es necesario especificar explícitamente el nombre del objetivo.

Ejemplo

# Objetivo personalizado: asociar el comando personalizado anterior
# ALL indica que se ejecuta automáticamente en la construcción predeterminada (make)
add_custom_target(generate_code ALL
    # Depende del archivo OUTPUT definido anteriormente
    DEPENDS ${CMAKE_BINARY_DIR}/generated_file.cpp
)

# Objetivo personalizado: ejecutar tareas auxiliares (no se ejecuta en la construcción predeterminada)
add_custom_target(deploy
    COMMAND ${CMAKE_COMMAND} -E echo "Desplegando en el servidor..."
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        ${CMAKE_BINARY_DIR}/bin
        /opt/ejemplo/deploy/
    COMMENT "Ejecutar operación de despliegue"
)

# Activar este objetivo por separado durante la construcción
# $ cmake --build . --target deploy

La diferencia entre add_custom_command y add_custom_target: el primero define una regla de «cómo generar archivos», activada por dependencias; el segundo define un objetivo de acción de «qué hacer», que debe invocarse explícitamente o ejecutarse automáticamente con ALL.


Multiplataforma y compilación cruzada

Una de las ventajas principales de CMake es el soporte multiplataforma: el mismo CMakeLists.txt puede generar los archivos de construcción correspondientes en diferentes sistemas operativos y arquitecturas.

Compilación multiplataforma

CMake detecta automáticamente el compilador y el entorno del sistema de la plataforma actual, y genera los archivos de construcción correspondientes.

También puedes especificar explícitamente información de la plataforma de destino mediante variables.

Variable Propósito Valor de ejemplo
CMAKE_SYSTEM_NAME Sistema operativo de destino Linux、Windows、Darwin、Android
CMAKE_SYSTEM_PROCESSOR Arquitectura del procesador de destino x86_64、arm、aarch64
CMAKE_C_COMPILER Ruta del compilador C /usr/bin/arm-linux-gnueabihf-gcc
CMAKE_CXX_COMPILER Ruta del compilador C++ /usr/bin/arm-linux-gnueabihf-g++

Compilación cruzada

La compilación cruzada consiste en compilar en la plataforma actual (por ejemplo, x86_64 Linux) un programa que se ejecutará en otra arquitectura (por ejemplo, ARM).

CMake utilizaarchivo de cadena de herramientas (Toolchain File)para especificar el compilador, la información del sistema y la configuración del enlazador necesarios para la compilación cruzada.

Crear el archivo de cadena de herramientas toolchain.cmake:

Ejemplo

# Ruta del archivo: toolchain.cmake
# Archivo de configuración de la cadena de herramientas de compilación cruzada ARM Linux

# El sistema de destino es Linux
set(CMAKE_SYSTEM_NAME Linux)

# La arquitectura del procesador de destino es ARM
set(CMAKE_SYSTEM_PROCESSOR arm)

# Especificar el compilador cruzado (obligatorio: la ruta debe ajustarse según la ubicación real de la cadena de herramientas)
set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)

# Especificar la ruta del sistema de archivos raíz del sistema de destino (opcional, para buscar bibliotecas y archivos de cabecera)
set(CMAKE_FIND_ROOT_PATH /path/to/arm-sysroot)

# Al buscar programas, buscar solo en las rutas del sistema de destino (no en las rutas del host)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# Al buscar bibliotecas, buscar solo en las rutas del sistema de destino
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)

# Al buscar archivos de cabecera, buscar solo en las rutas del sistema de destino
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

Usar el archivo de cadena de herramientas para la compilación cruzada:

# 配置时通过 -DCMAKE_TOOLCHAIN_FILE 指定工具链文件
mkdir build_arm && cd build_arm
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake ..

# 使用生成的 Makefile 编译(将调用 ARM 交叉编译器)
make

El archivo de cadena de herramientas debe especificarse al ejecutar cmake por primera vez. Si ya existe CMakeCache.txt, es necesario eliminar el directorio build y reconfigurar para cambiar de cadena de herramientas.


Propiedades y configuración de objetivos

CMake permite controlar con precisión las opciones de compilación, las opciones de enlazado y varios atributos a nivel de objetivo; esta es la práctica recomendada en el CMake moderno.

Propiedades de objetivos

set_target_properties()Se utilizan para establecer propiedades a nivel de objetivo; estas propiedades afectan el comportamiento del compilador y del enlazador.

Propiedad Propósito Ejemplo
COMPILE_OPTIONS Establecer opciones de compilación -Wall、-O2
COMPILE_DEFINITIONS Establecer macros de preprocesador DEBUG、VERSION=2
LINK_FLAGS Establecer banderas del enlazador -L/path/to/lib
OUTPUT_NAME Modificar el nombre del archivo de salida El nombre del objetivo es MyApp, la salida es my_app

Ejemplo

# Crear el objetivo de archivo ejecutable
add_executable(MyApp main.cpp)

# Establecer opciones de compilación y banderas de enlazado para el objetivo
set_target_properties(MyApp PROPERTIES
    COMPILE_OPTIONS "-Wall;-Wextra"
    COMPILE_DEFINITIONS "EJEMPLO_DEBUG"
    LINK_FLAGS "-L/usr/local/lib"
    OUTPUT_NAME "my_app"
)

# El set_target_properties anterior equivale al efecto de las siguientes tres directivas:
# target_compile_options(MyApp PRIVATE -Wall -Wextra)
# target_compile_definitions(MyApp PRIVATE EJEMPLO_DEBUG)
# target_link_options(MyApp PRIVATE -L/usr/local/lib)

En el CMake moderno, se recomienda usar target_compile_options(), target_compile_definitions() y target_link_options() en lugar de set_target_properties(), ya que admiten control de visibilidad PUBLIC/PRIVATE/INTERFACE y las dependencias son más explícitas.

Opciones personalizadas de compilación y enlace

Usar las directivas de la serie target permite controlar con mayor precisión el alcance de propagación de las opciones.

Ejemplo

# Establecer opciones de compilación para MyApp
# PRIVATE: solo MyApp usa estas opciones al compilarse
target_compile_options(MyApp PRIVATE -Wall -Wextra -Wpedantic)

# Establecer definiciones de preprocesador para MyApp
# Equivale a escribir #define EJEMPLO_VERSION "1.0" en el archivo fuente
target_compile_definitions(MyApp PRIVATE EJEMPLO_VERSION="1.0")

# Establecer opciones de enlazado para MyApp
target_link_options(MyApp PRIVATE -L/usr/local/lib)

# Si es una biblioteca, usar PUBLIC permite que los objetivos que enlazan esta biblioteca hereden estas opciones
# Por ejemplo: cualquier objetivo que use MyLib habilitará automáticamente C++17
target_compile_features(MyLib PUBLIC cxx_std_17)

Consideraciones

CMAKE_BUILD_TYPE solo es válido para generadores de configuración única.Si el proyecto necesita admitir tanto Makefile como Visual Studio, no dependas del valor de CMAKE_BUILD_TYPE. Usa expresiones de generador$<$<CONFIG:Debug>:DEBUG_VALUE>para cambiar dinámicamente según la configuración real.

Prioriza el uso de las directivas de la serie target_xxx.Aunque set_target_properties es potente, carece de control de visibilidad. En escenarios que requieren semántica PUBLIC/PRIVATE/INTERFACE, usar directivas especializadas como target_compile_options es una mejor opción.

El archivo de la cadena de herramientas debe especificarse en la primera ejecución de cmake.Al realizar compilación cruzada, si se vuelve a especificar la cadena de herramientas en un directorio que ya tiene CMakeCache.txt, se puede generar una configuración incompleta. Configure siempre la compilación cruzada en un directorio de build limpio.

Se recomienda añadir un prefijo a los nombres de las funciones en los módulos personalizados.Evite conflictos de nombres con los comandos integrados de CMake u otros módulos. Por ejemplo, use el nombre del proyecto como prefijo: myproject_set_warnings().

Otras extensiones