Práctica de CMake

Este artículo demostrará cómo usar CMake para gestionar un proyecto C++ de complejidad media, desde la creación del proyecto hasta el flujo completo de compilación y ejecución.

El contenido cubre la configuración básica, el enlazado de bibliotecas, las pruebas unitarias, los comandos personalizados y características avanzadas como la compilación cruzada multiplataforma.


Flujo de compilación de CMake

La siguiente figura muestra el flujo completo de compilación de CMake desde el código fuente hasta el archivo ejecutable final:

CMake 构建流程


Resumen del proyecto

Construiremos un proyecto C++ que contiene un programa principal, una biblioteca estática y pruebas unitarias.

La biblioteca proporciona funciones de operaciones matemáticas (suma, resta, multiplicación), el programa principal llama a estas funciones y el código de prueba verifica su correctitud.

Estructura del directorio del proyecto

MyProject/
├── CMakeLists.txt          # 根构建配置(项目定义、全局设置)
├── src/
│   ├── main.cpp            # 主程序入口
│   ├── CMakeLists.txt      # src 子目录构建配置(库 + 可执行文件)
│   ├── lib/
│   │   ├── module1.cpp     # 数学模块一:加法、减法
│   │   └── module2.cpp     # 数学模块二:乘法
│   └── include/
│       └── mylib.h         # 公共头文件(函数声明)
└── tests/
    ├── test_main.cpp       # 单元测试用例
    └── CMakeLists.txt      # 测试子目录构建配置

Out-of-Source Build (compilación fuera del directorio de fuentes)Es la mejor práctica de CMake: todos los artefactos de compilación (Makefile, archivos objeto, ejecutables) se colocan en un directorio build independiente, sin contaminar el directorio del código fuente.


Crear el archivo CMakeLists.txt

CMakeLists.txt es el archivo de configuración central de CMake, con una estructura jerárquica; hay uno en el directorio raíz y uno en cada subdirectorio.

CMakeLists.txt del directorio raíz

La configuración raíz define la información a nivel de proyecto y gestiona los subdirectorios mediante add_subdirectory.

Ejemplo

cmake_minimum_required(VERSION 3.10)    # Especificar el requisito de versión mínima de CMake
project(MyProject VERSION 1.0)          # Definir el nombre del proyecto y el número de versión

# Establecer el estándar C++ como C++11 y marcarlo como obligatorio
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Agregar ruta de búsqueda global de archivos de cabecera (PROJECT_SOURCE_DIR es el directorio raíz del proyecto)
include_directories(${PROJECT_SOURCE_DIR}/src/include)

# Agregar subdirectorios; CMake procesará sus CMakeLists.txt en orden
add_subdirectory(src)      # Procesar bibliotecas y ejecutables
add_subdirectory(tests)    # Procesar pruebas

CMakeLists.txt del directorio src

Este archivo define el objetivo de biblioteca (MyLib) y el objetivo de ejecutable (MyExecutable), y establece las dependencias.

Ejemplo

# Crear el objetivo de biblioteca estática MyLib, compuesto por dos archivos fuente
add_library(MyLib STATIC
    lib/module1.cpp
    lib/module2.cpp
)

# Especificar la ruta pública de archivos de cabecera para MyLib (PUBLIC indica que los usuarios también heredarán esta ruta)
target_include_directories(MyLib PUBLIC ${CMAKE_SOURCE_DIR}/src/include)

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

# Enlazar MyLib al ejecutable (PRIVATE indica que solo MyExecutable lo necesita)
target_link_libraries(MyExecutable PRIVATE MyLib)

Diferencia entre PRIVATE y PUBLIC:PRIVATEIndica que la dependencia solo la necesita el objetivo actual,PUBLICIndica que la dependencia se transmite a otros objetivos que enlazan con el objetivo actual.

CMakeLists.txt del directorio tests

La configuración de pruebas utiliza el framework Google Test; primero se debe buscar e incluir el paquete GTest.

Ejemplo

# Agregar el siguiente contenido en src/CMakeLists.txt

# Imprimir un mensaje informativo después de que MyExecutable se compile
add_custom_command(
    TARGET MyExecutable
POST_BUILD # Ejecutar después de la compilación
COMMAND ${CMAKE_COMMAND} -E echo "¡MyExecutable compilado!"
COMMENT "Imprimir información de compilación completada" # Descripción que se muestra en el registro de compilación
)

Objetivo personalizado (add_custom_target)

Un objetivo personalizado es un objetivo independiente que no produce archivos de salida y se utiliza para ejecutar tareas específicas.

Ejemplo

# Agregar el siguiente contenido en src/CMakeLists.txt

# Crear el objetivo run, que ejecuta el programa compilado
add_custom_target(run
    COMMAND ${CMAKE_BINARY_DIR}/src/MyExecutable
    DEPENDS MyExecutable                # Depende de MyExecutable para asegurar que se compile primero
    COMMENT "Ejecutar MyExecutable"
)

Ejecutar el objetivo personalizado:

# 在 build 目录下执行
make run
# o 
cmake --build . --target run

Diferencia entre comando personalizado y objetivo personalizado:add_custom_commandSe adjunta a un objetivo y se activa en una etapa específica de la compilación de ese objetivo;add_custom_targetEs un objetivo independiente que solo se ejecuta si se especifica explícitamente.


Multiplataforma y compilación cruzada

Una de las principales ventajas de CMake es el soporte multiplataforma, que permite cambiar fácilmente la plataforma y la arquitectura objetivo.

Especificar la plataforma objetivo

La plataforma objetivo se puede especificar mediante la variable CMAKE_SYSTEM_NAME.

# 为 Linux 平台交叉编译
cmake -DCMAKE_SYSTEM_NAME=Linux ..

# 为 Windows 平台交叉编译
cmake -DCMAKE_SYSTEM_NAME=Windows ..

Uso del archivo de toolchain

Para escenarios complejos de compilación cruzada, se recomienda utilizar un archivo de toolchain para gestionar de forma centralizada toda la configuración relacionada con la plataforma.

Primero, cree el archivo de toolchain toolchain.cmake:

Ejemplo

# Ruta del archivo: MyProject/toolchain.cmake
# Configuración del toolchain de compilación cruzada ARM Linux

# Especificar el sistema objetivo
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# Especificar la ruta del compilador cruzado (ajustar según el entorno real)
set(CMAKE_C_COMPILER   arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

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

# Buscar programas solo en el sysroot objetivo (no en el sistema host)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# Buscar archivos de biblioteca solo en el sysroot objetivo
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
# Buscar archivos de cabecera solo en el sysroot objetivo
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

Compilar usando el archivo de toolchain:

# 在 build 目录下执行
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake ..
cmake --build .

El archivo de toolchain centraliza la gestión de configuraciones relacionadas con la plataforma, como el compilador, el enlazador y las rutas de bibliotecas, de modo que cambiar de plataforma objetivo solo requiere especificar un archivo de toolchain diferente, sin modificar CMakeLists.txt.


Tipos de compilación y opciones comunes

CMake admite varios tipos de compilación, controlados mediante la variable CMAKE_BUILD_TYPE.

Tipo de compilación Opciones de compilación Escenario aplicable
Debug -g (símbolos de depuración) Etapa de desarrollo y depuración, se requieren puntos de interrupción e inspección de variables
Release -O2 -DNDEBUG (optimización + eliminación de aserciones) Publicación en producción, se busca velocidad de ejecución
RelWithDebInfo -O2 -g (optimización + símbolos de depuración) Versión de publicación que requiere análisis de rendimiento u optimización de depuración
MinSizeRel -Os (optimización de tamaño) Sistemas embebidos o escenarios sensibles al tamaño

Especificar el tipo de compilación:

# Debug 构建
cmake -DCMAKE_BUILD_TYPE=Debug ..

# Release 构建
cmake -DCMAKE_BUILD_TYPE=Release ..
Otras extensiones