CMake Moderno: Gestión orientada a targets y propiedades

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:

  1. math_lib (Target de tipo STATIC): Al definirlo con add_library, creamos un objeto con sus propias propiedades. Cuando usamos target_include_directories con la palabra clave PUBLIC sobre ${CMAKE_CURRENT_BINARY_DIR}/include, le estamos diciendo a CMake: “Para compilar math_lib, busca en esta carpeta, y si alguien más enlaza a math_lib, dile que también busque ahí”.
  2. Propagación de la visibilidad: Observa target_compile_definitions(math_lib PRIVATE MATH_INTERNAL_DEBUG). Al ser PRIVATE, si compilas math_lib, el macro estará disponible y verás el mensaje [LOG INTERNO]. Sin embargo, cuando compilamos app, el compilador de app nunca conocerá la existencia de MATH_INTERNAL_DEBUG, manteniendo el código de nuestro ejecutable limpio de macros internas de la librería.
  3. Requerimientos de estándar: Con target_compile_features(math_lib PUBLIC cxx_std_20), hemos creado una dependencia de capacidad. Como math_lib es un componente público, CMake asegura que app se compile con soporte para C++20, garantizando que el ABI (Application Binary Interface) sea compatible y que los tipos de C++20 (como std::span o std::format si se usaran) funcionen correctamente en toda la cadena de compilación.
  4. app (Target de tipo executable): Al usar target_link_libraries(app PRIVATE math_lib), el ejecutable “consume” las propiedades PUBLIC de la librería (sus includes y su estándar C++20), pero el enlace es PRIVATE porque el ejecutable es el final de la cadena; nadie más va a enlazar a app.

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.

129

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio