Si llevas tiempo usando CMake y ves código con include_directories, link_libraries o variables globales como CMAKE_CXX_FLAGS, estás ante CMake del pasado. El enfoque moderno (disponible desde CMake 3.13 y mejorado en versiones posteriores) gira en torno a targets y sus propiedades, con comandos que operan sobre targets específicos en lugar de modificar variables globales.
La diferencia no es solo estética: el modelo de targets hace que las dependencias sean explícitas, reproducibles y componibles. Dos librerías que definen targets no interfieren entre sí.
El modelo de targets
# CMake moderno: todo es un target
cmake_minimum_required(VERSION 3.20)
project(mi_proyecto VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # Desactiva extensiones GCC/Clang
add_library(mi_lib STATIC
src/mi_lib.cpp
src/utilidades.cpp
)
# Las propiedades son POR TARGET:
target_include_directories(mi_lib
PUBLIC include/ # visible para quienes usen mi_lib
PRIVATE src/internal/ # solo para mi_lib internamente
)
target_compile_features(mi_lib PUBLIC cxx_std_23)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mi_lib)
Las visibilidades PUBLIC, PRIVATE e INTERFACE controlan qué se propaga a los dependientes: PRIVATE solo afecta al target actual, PUBLIC también a quien lo enlaza, INTERFACE solo a quien lo enlaza (útil para librerías header-only).
Lo que ya no debes hacer
# MAL: modifica el entorno global, contamina a todos
include_directories(include/)
link_libraries(mi_lib)
add_definitions(-DDEBUG)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
# BIEN: afecta solo al target indicado
target_include_directories(mi_target PRIVATE include/)
target_link_libraries(mi_target PRIVATE mi_lib)
target_compile_definitions(mi_target PRIVATE DEBUG=1)
target_compile_options(mi_target PRIVATE -Wall -Wextra)
CMakePresets.json
CMake Presets (introducidos en CMake 3.19 y mejorados en versiones posteriores) permiten definir configuraciones reutilizables en un archivo JSON. Se acabó documentar en el README qué flags hay que pasar a mano:
// CMakePresets.json
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"displayName": "Debug GCC",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_COMPILER": "g++"
}
},
{
"name": "release",
"displayName": "Release con LTO",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON"
}
}
],
"buildPresets": [
{ "name": "debug", "configurePreset": "debug" },
{ "name": "release", "configurePreset": "release" }
]
}
# Usar los presets: cmake --preset debug cmake --build --preset debug cmake --preset release cmake --build --preset release
Gestión de dependencias: FetchContent y CPM
FetchContent (disponible desde CMake 3.11, mejorado en 3.14) descarga y configura dependencias externas como parte del build:
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(mi_app PRIVATE fmt::fmt)
CPM.cmake es un wrapper sobre FetchContent que añade caché y control de versiones más cómodo:
include(cmake/CPM.cmake)
CPMAddPackage("gh:fmtlib/fmt#10.2.1")
CPMAddPackage("gh:google/googletest#v1.14.0")
target_link_libraries(mi_app PRIVATE fmt::fmt)
target_link_libraries(tests PRIVATE GTest::gtest_main)
Testing integrado con CTest
enable_testing() add_executable(mis_tests tests/test_main.cpp) target_link_libraries(mis_tests PRIVATE mi_lib GTest::gtest_main) include(GoogleTest) gtest_discover_tests(mis_tests)
# Ejecutar tests: ctest --preset debug --output-on-failure
Sanitizers desde CMake
option(ENABLE_ASAN "Activar AddressSanitizer" OFF)
if(ENABLE_ASAN)
target_compile_options(mi_app PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(mi_app PRIVATE -fsanitize=address)
endif()
# Activar con preset o variable: cmake -DENABLE_ASAN=ON --preset debug
El siguiente artículo de esta serie cubre los sanitizers de C++ en detalle: qué detecta cada uno, cómo activarlos y cómo interpretar sus informes.
Estructura recomendada de proyecto
mi_proyecto/
??? CMakeLists.txt
??? CMakePresets.json
??? cmake/
? ??? CPM.cmake
??? include/
? ??? mi_lib/
? ??? api.hpp
??? src/
? ??? mi_lib.cpp
? ??? main.cpp
??? tests/
??? test_api.cpp
Separar headers públicos (include/) de los privados (src/) y de los tests (tests/) hace que las propiedades de los targets CMake se correspondan con la estructura del código.
Si tu proyecto usa módulos de C++20, el artículo sobre módulos en C++20 explica cómo integrarlos con FILE_SET CXX_MODULES en CMake 3.28+. Y si estás pensando en herramientas de análisis adicionales más allá de los sanitizers, clang-tidy se integra con CMake vía la propiedad CMAKE_CXX_CLANG_TIDY.
Imagen: Pexels / Rafael Minguet Delgado
