Un target en CMake es, en esencia, un objeto que encapsula todo lo necesario para construir un artefacto, ya sea un ejecutable o una librería. En lugar de gestionar el estado global del proyecto mediante variables de configuración, el enfoque moderno se basa en la encapsulación de propiedades. Funciona de esta manera porque permite que cada componente sea autosuficiente: tú defines qué necesita un target para compilarse y qué necesita un consumidor para utilizarlo. Debes usar este enfoque siempre que trabajes en proyectos con CMake 3.0 o superior; es la única forma de lograr dependencias limpias y reutilizables. Si intentas gestionar la configuración mediante variables globales, el sistema de construcción se vuelve frágil, ya que cualquier cambio en una parte del proyecto puede “contaminar” accidentalmente a otras que no deberían verse afectadas.
# Archivo único: Ejecuta 'cmake -B build -S . && cmake --build build'
cmake_minimum_required(VERSION 3.15)
project(ModernCMakeDemo LANGUAGES CXX)
# --- SIMULACIÓN DE ESTRUCTURA DE ARCHIVOS ---
# Para que este ejemplo sea un único bloque compilable, generamos los archivos en el directorio de build.
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/include/math_engine.hpp "
#pragma once
#include <string_view>
namespace math {
void print_version(std::string_view version);
int add(int a, int b);
}
")
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/src/math_engine.cpp "
#include \"math_engine.hpp\"
#include <iostream>
namespace math {
void print_version(std::string_view version) {
#ifdef MATH_INTERNAL_DEBUG // Definición PRIVATE
std::cout << \"[LOG INTERNO] Versión: \" << version << std::endl;
#endif
std::cout << \"Motor Matemático v\" << version << \" listo.\" << std::endl;
}
int add(int a, int b) { return a + b; }
}
")
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/main.cpp "
#include \"math_engine.hpp\"
#include <iostream>
int main() {
math::print_version(\"2.1.0\");
std::cout << \"Resultado de 5 + 3: \" << math::add(5, 3) << std::endl;
return 0;
}
")
# --- CONFIGURACIÓN DE LA LIBRERÍA (math_lib) ---
add_library(math_lib STATIC ${CMAKE_CURRENT_BINARY_DIR}/src/math_engine.cpp)
# PUBLIC: Los directorios de headers se usan para compilar la librería Y para quienes la usen.
# PRIVATE: Los directorios solo se usan para compilar la propia librería.
target_include_directories(math_lib
PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/include
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/src
)
# PUBLIC: Obliga a que cualquier target que enlace a math_lib también use C++20.
target_compile_features(math_lib PUBLIC cxx_std_20)
# PRIVATE: Esta macro solo existe durante la compilación de math_lib.
# No se propaga a los ejecutables que la utilicen.
target_compile_definitions(math_lib PRIVATE MATH_INTERNAL_DEBUG)
# --- CONFIGURACIÓN DEL EJECUTABLE (app) ---
add_executable(app ${CMAKE_CURRENT_BINARY_DIR}/main.cpp)
# PRIVATE: El ejecutable usa math_lib, pero math_lib es un detalle de implementación de app.
# No necesitamos que nada que use 'app' herede las propiedades de math_lib.
target_link_libraries(app PRIVATE math_lib)
# Nota: 'app' heredará automáticamente el include de math_lib y el estándar C++20
# gracias a que fueron declarados como PUBLIC en el target anterior.
Desglose del ejemplo
Analicemos cómo interactúan los objetos definidos en el CMakeLists.txt:
math_lib(Target de tipoSTATIC): Al definirlo conadd_library, creamos un objeto con sus propias propiedades. Cuando usamostarget_include_directoriescon la palabra clavePUBLICsobre${CMAKE_CURRENT_BINARY_DIR}/include, le estamos diciendo a CMake: “Para compilarmath_lib, busca en esta carpeta, y si alguien más enlaza amath_lib, dile que también busque ahí”.- Propagación de la visibilidad: Observa
target_compile_definitions(math_lib PRIVATE MATH_INTERNAL_DEBUG). Al serPRIVATE, si compilasmath_lib, el macro estará disponible y verás el mensaje[LOG INTERNO]. Sin embargo, cuando compilamosapp, el compilador deappnunca conocerá la existencia deMATH_INTERNAL_DEBUG, manteniendo el código de nuestro ejecutable limpio de macros internas de la librería. - Requerimientos de estándar: Con
target_compile_features(math_lib PUBLIC cxx_std_20), hemos creado una dependencia de capacidad. Comomath_libes un componente público, CMake asegura queappse compile con soporte para C++20, garantizando que el ABI (Application Binary Interface) sea compatible y que los tipos de C++20 (comostd::spanostd::formatsi se usaran) funcionen correctamente en toda la cadena de compilación. app(Target de tipoexecutable): Al usartarget_link_libraries(app PRIVATE math_lib), el ejecutable “consume” las propiedadesPUBLICde la librería (sus includes y su estándar C++20), pero el enlace esPRIVATEporque el ejecutable es el final de la cadena; nadie más va a enlazar aapp.
El error frecuente
El error más común en proyectos que migran de CMake “clásico” a “moderno” es el uso de comandos de configuración global como include_directories() o link_libraries().
# --- MAL: Estilo Legacy (Evitar) ---
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
link_libraries(math_lib)
Si utilizas include_directories() en el CMakeLists.txt raíz, estás aplicando ese directorio de cabeceras a todos los targets de todo el proyecto, incluso a herramientas de testeo o utilidades que no tienen nada que ver con la librería matemática. Esto causa colisiones de nombres (si dos librerías tienen un utils.h idéntico) y hace que el sistema de construcción sea extremadamente difícil de depurar, ya que las dependencias no son explícitas, sino que dependen de una “nube” de variables globales. Siempre utiliza target_xxx para delimitar el alcance de cada propiedad.
N° 129