· C · C++ · GCC · Make · CMake
Compilar
Compilar un programa de dos ficheros cabe en un solo comando. Este artículo parte de ahí y deja crecer el proyecto: cada herramienta aparece cuando la anterior deja de bastar, desde gcc hasta CMake y Ninja.
Programa de ejemplo
Un programa pequeño que imprime diez números aleatorios con distribución normal. Está repartido en varios ficheros y usa la biblioteca matemática, así que sirve para ver todo el proceso. Puedes descargar el código de ejemplo para seguir la página.
Fases de la compilación
Compilar es traducir código fuente a un programa que la máquina puede ejecutar. Aunque se le llame así a todo, son cuatro fases seguidas, y el resultado de cada una se puede ver parando gcc con -E, -S o -c.
main.c calc.cpp solver.f90 start.s main.i calc.ii main.s calc.s solver.s main.o calc.o solver.o start.o libclibmlibstdc++libgfortran
programa.o. Al enlazador no le importa de dónde viene cada uno.Errores frecuentes por fase
| Mensaje | Fase | Causa |
|---|---|---|
fatal error: random.h: No such file or directory | Preprocesado | No encuentra la cabecera |
expected ';' before... | Compilación | Error de sintaxis |
undefined reference to 'sqrt' | Enlazado | Falta una definición (aquí, -lm) |
multiple definition of... | Enlazado | Dos .o definen lo mismo |
Compilación manual con GCC
Antes de recurrir a ninguna herramienta conviene construir el programa invocando directamente al compilador. Todo lo que hacen Make, CMake o Ninja se reduce, al final, a estos mismos comandos.
Compilación en un solo comando
gcc main.c random.c -lm -o main
Con dos ficheros no hace falta nada más. El coste aparece al editar: cada cambio, por pequeño que sea, recompila todos los ficheros. En un proyecto de cientos de ficheros, eso significa esperar minutos por una línea.
Compilación y enlazado por separado
Las tres primeras fases trabajan con un solo fichero, así que se pueden guardar sus resultados y reutilizarlos:
gcc -c main.c
gcc -c random.c
gcc main.o random.o -lm -o main
El ejecutable es el mismo, pero ahora, si solo cambias random.c, basta con recompilar ese fichero y volver a enlazar.
-lm va después de los .o porque el enlazador procesa los ficheros en orden y usa de cada biblioteca lo que esté pendiente hasta ese momento. Con gcc -lm main.o random.o falla con undefined reference.
.c se compila por separado. El enlazador une los objetos y resuelve las llamadas entre ellos y a las bibliotecas.Declaración y definición
- Un
.hdeclara: dice qué existe y con qué firma. - Un
.cdefine: dice cómo funciona. - Solo un
.opuede definir cada función. Si dos.ola definen, el enlazador no sabe cuál usar. - El
#ifndef RANDOM_Hevita que la cabecera se procese dos veces si un mismo fichero la incluye más de una vez.
Automatización con Make
Compilar por separado ahorra tiempo, pero traslada el trabajo a tu memoria: hay que saber qué ficheros han cambiado, qué depende de ellos y en qué orden repetir los comandos. Si olvidas uno, ejecutas código antiguo sin darte cuenta.
Make se encarga de esa contabilidad. Le describes qué depende de qué y, comparando las fechas de modificación, rehace solo lo que ha quedado desactualizado.
Reglas explícitas
Makefile
all: main
main: main.o random.o
gcc main.o random.o -o main -lm
main.o: main.c random.h
gcc -c main.c
random.o: random.c
gcc -c random.c
clean:
rm -f main *.o
Cada regla tiene tres componentes:
objetivo: dependencias
receta
- Objetivo: lo que se quiere construir, normalmente un fichero.
- Dependencias: lo que necesita. Si alguna es más nueva que el objetivo, este se reconstruye.
- Receta: los comandos que lo construyen.
all y clean no crean ningún fichero con ese nombre, son nombres para una acción.
Otros elementos del ejemplo:
- Objetos (
main.o,random.o): el resultado de compilar cada.c, código máquina todavía sin enlazar. Son los que permiten recompilar solo lo que cambia. -c: compila sin enlazar y genera el.o.-o main: nombre del fichero de salida.-lm: enlaza con la biblioteca matemática (-l+m= libm).
Al ejecutar make se construye el primer objetivo, all. El flujo es:
- Make ve que
allnecesitamain, y este necesita los dos.o. - Compila cada
.oque falte o sea más antiguo que sus dependencias (fases 1 a 3). - Enlaza los objetos con
-lmpara crearmain(fase 4). - Si lo lanzas de nuevo sin cambios, no hace nada. Si editas
random.c, solo recompilarandom.oy vuelve a enlazar. Si editasrandom.h, recompilamain.o.
Tabuladores obligatorios. Las recetas deben empezar por un tabulador. Si el editor lo convierte en espacios, verás el error missing separator.
Variables y reglas de patrón
El Makefile anterior funciona, pero crece con el proyecto: cada .c nuevo exige otra regla casi idéntica, y las opciones del compilador se repiten en todas. Las variables y las reglas de patrón eliminan esa repetición:
Makefile
CC = gcc
# -Wall: Enable ALL warnings
# -Wextra: Enable extra warnings
# -g: Include debugging info
CFLAGS = -Wall -Wextra -g
TARGET = main
SRCS = main.c random.c
OBJS = $(SRCS:.c=.o)
LIBS = -lm
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $(OBJS) -o $(TARGET) $(LIBS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(TARGET) *.o
| Elemento | Qué hace |
|---|---|
CC, CFLAGS, LIBS | Compilador, opciones y bibliotecas, definidos en un único sitio. |
$(SRCS:.c=.o) | Sustituye la extensión: se convierte en main.o random.o. |
%.o: %.c | Regla de patrón: cualquier .o se crea a partir del .c del mismo nombre. |
$< | La primera dependencia, por ejemplo main.c. |
$@ | El objetivo de la regla, por ejemplo main.o. |
Para añadir un fichero nuevo basta con ampliar SRCS. Pero hemos pagado un precio: esta regla de patrón no sabe nada de las cabeceras. Si editas random.h, make responde que no hay nada que hacer y sigues con objetos desactualizados.
Generación de la build con CMake
El problema de random.h tiene arreglo dentro de Make, pero es una muestra de lo que viene cuando el proyecto sigue creciendo:
- Dependencias. Las de las cabeceras hay que mantenerlas a mano o añadir más reglas para generarlas.
- Otros sistemas. Las recetas son comandos de shell.
gcc,rmo-lmno se traducen por sí solos a Windows con MSVC. - Variantes. Tener builds de depuración y de producción exige duplicar opciones o añadir lógica condicional.
Todo apunta a lo mismo: el Makefile se ha vuelto demasiado complejo para escribirlo a mano, así que conviene generarlo. Eso hace CMake. No compila por sí mismo: a partir de una descripción del proyecto, genera los ficheros de Make, Ninja, Visual Studio o Xcode.
Flujo de trabajo
- Defines el proyecto en un fichero
CMakeLists.txt. - Ejecutas
cmakepara crear automáticamente el Makefile. - Ejecutas
makepara construir el proyecto. - Si solo editas el código existente, basta con volver a ejecutar
make. - Si añades ficheros
.co.cppo cambias dependencias, editas tambiénCMakeLists.txt.
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(main)
add_executable(main main.c random.c random.h)
target_link_libraries(main PRIVATE m)
project: nombre del proyecto.add_executable: el ejecutable y sus fuentes.target_link_libraries: equivale a-lm.
No se menciona ninguna regla ni ningún comando, y el problema de las cabeceras desaparece: en el Makefile generado, editar random.h recompila main.c.
Builds fuera del código fuente
Con Make todo quedaba junto al código. Con CMake, los ficheros generados se ubican en otro directorio, normalmente build. El código queda limpio y puedes borrar esa carpeta sin perder nada.
mkdir build && cd ./build
cmake ..
make
Variantes de build
La lógica condicional también sobra. Cada variante vive en su propia carpeta, con el mismo CMakeLists.txt:
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake -DCMAKE_BUILD_TYPE=Release ..
Generadores
Un único CMakeLists.txt sirve para distintas alternativas: Make, Ninja, Visual Studio y Xcode. Se eligen con la opción -G.
Construcción rápida con Ninja
Queda un último límite, y solo se nota a gran escala: el propio Make tarda en decidir qué hacer. En Chromium necesitaba más de 10 segundos para empezar a construir, incluso con poco que recompilar. Ninja lo hacía en menos de uno.
Ninja hace el mismo trabajo que Make: lee las dependencias de un fichero (build.ninja) y orquesta la compilación. Pero está pensado para que lo genere una herramienta como CMake, no para escribirlo a mano. Como ya no escribimos el Makefile, cambiar de uno a otro no toca el proyecto:
mkdir ninja && cd ./ninja
cmake -G Ninja ..
ninja
Resumen
- Compilar son cuatro fases: preprocesado, compilación, ensamblado y enlazado. Cada lenguaje llega por su camino al mismo
.o, y el enlazador une las piezas. - Compilar por separado evita recompilarlo todo, pero obliga a recordar qué ha cambiado.
- Make lleva esa cuenta: a partir de las dependencias, solo rehace lo desactualizado.
- CMake evita escribir el Makefile a mano: resuelve las cabeceras, los distintos sistemas y las variantes de build.
- Ninja sustituye a Make en proyectos grandes, porque arranca mucho más rápido.
Como las dependencias quedan descritas, además se puede compilar en paralelo con make -j o con ninja, que ya lo hace por defecto.
| Herramienta | Papel | Cuándo usarla |
|---|---|---|
| GCC / Clang | Compilan y enlazan | Siempre: el resto acaba llamando a un compilador |
| Make | Decide qué reconstruir | Proyectos pequeños, o para aprender cómo funciona |
| CMake | Genera los ficheros de construcción | Proyectos que crecen o que deben funcionar en varios sistemas |
| Ninja | Decide qué reconstruir, más rápido | Proyectos grandes, generado desde CMake |