CMake moderno en 2026: targets, presets y lo que ya no debes hacer

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

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP