CMake, Autotools y el panorama
Por qué sobre Make se construyeron generadores: Autotools y su rastro histórico, CMake como estándar de facto, Ninja como motor de ejecución y los sistemas herméticos. Criterios reales para elegir.
Make resuelve un problema y crea otro: te obliga a describir a mano un grafo cuyos detalles dependen del compilador, del sistema operativo y de las bibliotecas instaladas. Toda la historia posterior de los sistemas de construcción en C es la historia de generar ese grafo en lugar de escribirlo. Autotools lo generó a base de probar la máquina; CMake lo generó a base de describir intenciones; los sistemas herméticos lo generaron negándose a confiar en la máquina.
- Explicar qué problema histórico resolvió Autotools y por qué su rastro sigue vivo.
- Situar CMake como generador de grafos y entender el modelo de destinos con propiedades.
- Separar la fase de configuración de la de ejecución, y el papel de Ninja en la segunda.
- Elegir sistema de construcción con criterios de ecosistema, no de gusto personal.
Autotools: portabilidad por interrogatorio
En los años ochenta y noventa no existía una plataforma Unix, existían docenas incompatibles. Ninguna cabecera estaba garantizada, ninguna función tenía la misma firma en todas partes y ningún compilador aceptaba las mismas banderas. Autotools resolvió el problema del único modo que quedaba: no asumir nada y comprobarlo todo en la máquina de destino, antes de compilar.
El resultado es la secuencia canónica que has ejecutado cien veces sin mirarla.
./configure --prefix=/usr/local # interroga la maquina y genera el Makefile
make # construye con el Makefile generado
make install # copia a las rutas descubiertas
El guion configure es un programa de shell portable, con frecuencia de decenas de miles de líneas, generado por autoconf a partir de configure.ac. Su trabajo es compilar y ejecutar cientos de fragmentos diminutos de C para responder preguntas como si existe una cabecera concreta, cuánto ocupa un tipo o si el enlazador acepta cierta opción. Cada respuesta se escribe en config.h como una macro, y el código fuente se llena de compilación condicional para adaptarse.
El coste es notorio: la fase de configuración puede tardar más que la compilación entera, y el ecosistema —autoconf, automake, libtool, pkg-config, m4— es célebre por su dificultad. Pero sería un error despacharlo como reliquia. Autotools sigue construyendo buena parte de GNU, funciona en sistemas donde ninguna herramienta moderna arranca y solo exige una shell POSIX y un compilador en la máquina de destino. Y una de sus piezas ganó por separado: pkg-config es hoy el mecanismo universal para preguntar por las banderas de una biblioteca instalada, y lo usan CMake y Meson por igual.
Autotools separó tres máquinas que suelen ser la misma pero no tienen por qué serlo: aquella donde se construye, aquella donde se ejecutará el binario y, para un compilador, aquella para la que ese binario genera código. Esa terminología es la base de todo el soporte de compilación cruzada que existe hoy, y cualquier sistema que la ignore fracasa en cuanto sales del escritorio.
CMake: describir destinos, no comandos
CMake invierte el planteamiento. En lugar de escribir el grafo o de deducirlo probando la máquina, describes destinos con propiedades y dejas que CMake genere el grafo concreto para el generador que elijas: Makefiles, Ninja, proyectos de Visual Studio o de Xcode.
cmake_minimum_required(VERSION 3.28)
project(demo LANGUAGES C)
set(CMAKE_C_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_library(nucleo STATIC src/parser.c src/util.c)
target_include_directories(nucleo PUBLIC include)
target_compile_options(nucleo PRIVATE -Wall -Wextra)
add_executable(prog src/main.c)
target_link_libraries(prog PRIVATE nucleo)
La idea central es la propagación de propiedades por el grafo de destinos. Una propiedad marcada como pública viaja a todo lo que enlace contra ese destino; una privada se queda en él. Así, prog hereda automáticamente el directorio de inclusión de nucleo sin que nadie lo repita. Ese modelo, llamado CMake moderno para distinguirlo de las variables globales de la era anterior, es lo que convirtió a CMake en el estándar de facto de C y C++.
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build
cmake --install build --prefix /usr/local
CMake no es agradable. Su lenguaje trata todo como cadenas y listas separadas por punto y coma, arrastra veinte años de compatibilidad y su documentación es enorme y desigual. Se elige por gravedad: si tu proyecto va a ser consumido por otros proyectos de C o C++, o si necesitas Windows como plataforma de primera clase, el ecosistema espera un fichero de CMake y proporcionarlo es un servicio a tus usuarios.
Configurar y ejecutar son fases distintas
El panorama moderno se entiende mejor separando dos responsabilidades que Make mezclaba.
flowchart LR D[Descripcion de alto nivel] --> C[Fase de configuracion] C --> G[Grafo concreto generado] G --> E[Fase de ejecucion] E --> B[Binarios] C -.CMake Meson Autotools.-> G E -.Ninja o Make.-> B style C fill:#89b4fa,color:#11111b style E fill:#f9e2af,color:#11111b style B fill:#a6e3a1,color:#11111b
La configuración decide qué compilador hay, qué bibliotecas están disponibles, qué opciones se activan y qué comandos habrá que ejecutar. Es lenta, se hace pocas veces y es donde vive toda la complejidad. La ejecución recorre el grafo ya resuelto y lanza los comandos que hagan falta. Es rápida, se hace constantemente y debería ser aburrida.
Ninja existe solo para la segunda fase y está diseñada bajo una premisa explícita: nadie escribe Ninja a mano. Su formato no tiene condicionales, ni funciones, ni bucles; es una lista plana de comandos y dependencias. A cambio, su recorrido del grafo y su detección de cambios son mucho más rápidos que los de Make, que en cada invocación reinterpreta un lenguaje entero. Meson y CMake generan Ninja por defecto por esa razón.
Meson ocupa la posición intermedia. Describe destinos como CMake, pero con un lenguaje deliberadamente pequeño, sin funciones definidas por el usuario ni recursión, y con valores por defecto que ya son los correctos.
project('demo', 'c', default_options: ['c_std=c23', 'warning_level=3'])
nucleo = static_library('nucleo', 'src/parser.c', 'src/util.c',
include_directories: include_directories('include'))
executable('prog', 'src/main.c', link_with: nucleo)
La restricción es intencionada: al no ser un lenguaje de propósito general, el fichero de construcción no puede degenerar en un programa que nadie entienda, que es exactamente el destino de casi todos los ficheros de CMake grandes que verás en producción.
En el extremo opuesto están los sistemas herméticos, como Bazel o Nix, que sustituyen la marca de tiempo por un hash del contenido de todas las entradas, incluidas las herramientas, y aíslan cada acción para que no pueda leer nada que no haya declarado. Eso les da reproducibilidad exacta y caché compartida entre máquinas, a cambio de una rigidez y una curva de adopción considerables.
Cómo elegir
Make
Proyectos pequeños, herramientas internas, entornos empotrados o cualquier sitio donde no quieras dependencias. Sin rival en ubicuidad.
CMake
Bibliotecas destinadas a ser consumidas por terceros, proyectos multiplataforma con Windows, cualquier cosa que toque C++.
Meson
Proyectos nuevos de C que no necesitan el ecosistema de CMake. Sintaxis limpia, valores por defecto sensatos, genera Ninja.
Bazel o Nix
Monorepos grandes, requisitos de reproducibilidad estricta, caché distribuida. Coste de adopción alto y justificado solo a escala.
| Ecosistema | Lo que espera encontrar |
|---|---|
| Kernel de Linux y proyectos GNU | Make y Autotools |
| Bibliotecas de C y C++ de terceros | CMake, con destinos exportados |
| GNOME, systemd, GStreamer | Meson |
| Empotrados y bootloaders | Make, a menudo con Kconfig |
| Monorepos corporativos | Bazel o equivalentes internos |
La compilación cruzada es el mejor examen para cualquiera de ellos, porque obliga a separar limpiamente lo que se ejecuta durante la construcción de lo que se ejecutará después. Autotools lo resuelve con sus tres máquinas, CMake con ficheros de cadena de herramientas y Meson con ficheros de máquina cruzada. Make no lo resuelve en absoluto: te deja a ti la tarea de no confundir ambos mundos, y por eso un Makefile que funciona en el escritorio suele romperse en cuanto apunta a un microcontrolador.
La pregunta correcta no es cuál es mejor sino quién va a consumir tu proyecto. Un sistema de construcción es, ante todo, una interfaz pública: define cómo un desconocido compila tu código, cómo un empaquetador lo integra en una distribución y cómo otro proyecto lo enlaza. Elegir contra la corriente de tu ecosistema traslada ese coste a todos los demás.
Cinco décadas de herramientas y una sola idea persistente: describir un grafo de artefactos, decidir qué está obsoleto y ejecutar lo mínimo. Lo que cambia entre Make, Autotools, CMake, Meson, Ninja y Bazel no es esa idea sino dos parámetros. El primero es quién construye el grafo: tú a mano en Make, un interrogatorio de la máquina en Autotools, un generador a partir de una descripción declarativa en CMake y Meson. El segundo es qué cuenta como cambio: la marca de tiempo en Make, el hash del contenido en Bazel y Nix, el hash del contenido junto con el de la herramienta y el entorno en los sistemas herméticos. Todo lo demás —sintaxis, ergonomía, velocidad— es implementación. Y la trayectoria histórica apunta en una dirección clara: cada generación desplaza más decisiones desde el humano hacia el sistema, y estrecha más la definición de qué se considera una entrada legítima, porque cada entrada no declarada es una fuente de builds que funcionan en tu máquina y en ninguna otra. Cuando entiendas que un sistema de construcción es un evaluador incremental de funciones puras sobre un sistema de ficheros, cambiar de herramienta dejará de ser aprender un lenguaje nuevo y pasará a ser aprender dónde escribe cada una sus dos respuestas.
- Descarga un proyecto que use Autotools, ejecuta
./configurecon la salida detallada y examina elconfig.hresultante. Identifica cinco comprobaciones y explica qué incompatibilidad histórica resuelve cada una. - Convierte tu Makefile genérico a CMake usando destinos, propiedades públicas y privadas, y comprueba que un consumidor hereda los directorios de inclusión sin declararlos.
- Genera el mismo proyecto con Make y con Ninja desde CMake y compara los tiempos de una reconstrucción sin cambios.
- Escribe la versión Meson del mismo proyecto y compara la longitud y la legibilidad de las tres descripciones.
- Elige una biblioteca de tu sistema, resuelve sus banderas con
pkg-configy enlaza contra ella desde los tres sistemas.