1. De un directorio a un proyecto
Los fuentes son los de los dos temas anteriores, pero ahora ordenados como se hace en la práctica: encabezados públicos en include/, código en src/, pruebas en tests/, y todo lo generado en un directorio aparte, build/, que se puede borrar entero sin riesgo.
proy/
├── Makefile
├── include/
│ └── util.h
├── src/
│ ├── main.c
│ ├── numeros.c
│ └── texto.c
├── tests/
│ └── prueba.c
└── build/ ← lo genera make; no se versiona
├── debug/
└── release/
Los módulos numeros.c y texto.c forman la biblioteca libbasico (tema 32); main.c es el programa que la usa y tests/prueba.c, un ejecutable que verifica sus funciones y devuelve un código distinto de cero si algo falla:
#include <stdio.h>
#include <string.h>
#include "util.h"
static int fallos;
#define VERIFICAR(cond) \
do { \
if (!(cond)) { \
printf("FALLO: %s (linea %d)\n", #cond, __LINE__); \
fallos++; \
} \
} while (0)
int main(void)
{
int v[] = {4, 9, 2, 7};
char s[] = "abc";
VERIFICAR(maximo(v, 4) == 9);
VERIFICAR(promedio(v, 4) == 5.5);
invertir(s);
VERIFICAR(strcmp(s, "cba") == 0);
printf("%s\n", fallos ? "hay fallos" : "todas las pruebas pasan");
return fallos != 0;
}
Los demás archivos (util.h, numeros.c, texto.c, main.c) son los del tema 35, movidos de carpeta. El Makefile se construye por partes en las secciones siguientes y el listado completo está en la sección 9.
2. Dependencias automáticas: -MMD -MP
La solución a «make no lee los #include» es que lo haga quien sí los lee: el compilador. Con la opción -MMD, gcc escribe, junto al objeto, un archivo .d con una regla de make que lista el fuente y todos los encabezados propios que incluyó (-MD incluye también los del sistema, que casi nunca cambian). -MP agrega una regla vacía por cada encabezado, para el caso que se explica abajo. Basta añadirlas a CPPFLAGS y cargar los .d con include:
CPPFLAGS = -Iinclude -MMD -MP
DEPS = $(LIBOBJ:.o=.d) $(MAINOBJ:.o=.d) $(TESTOBJ:.o=.d)
-include $(DEPS)
El guion de -include hace que make no proteste si algún .d no existe todavía, como ocurre la primera vez (sin .o previo tampoco hay .d, y todo se va a compilar de todos modos). Lo que contiene un .d se ve después de compilar:
$ make
CC src/main.c
CC src/numeros.c
CC src/texto.c
AR build/debug/libbasico.a
LINK build/debug/prog
$ find build -type f | sort
build/debug/libbasico.a
build/debug/main.d
build/debug/main.o
build/debug/numeros.d
build/debug/numeros.o
build/debug/prog
build/debug/texto.d
build/debug/texto.o
$ cat build/debug/numeros.d
build/debug/numeros.o: src/numeros.c include/util.h
include/util.h:
La primera línea es una regla común, escrita por el compilador: «numeros.o depende de numeros.c y de util.h». La segunda es la regla vacía de -MP. Con esto el Makefile ya no necesita saber qué encabezados incluye cada fuente: cada compilación actualiza su lista. Se puede comprobar que reacciona con precisión:
$ make
make: Nothing to be done for 'all'.
$ touch include/util.h; make
CC src/main.c
CC src/numeros.c
CC src/texto.c
AR build/debug/libbasico.a
LINK build/debug/prog
$ touch src/texto.c; make
CC src/texto.c
AR build/debug/libbasico.a
LINK build/debug/prog
El encabezado lo incluyen los tres fuentes, y los tres se recompilan. El fuente solo afecta a su objeto, y de él sube a la biblioteca y al programa. Es exactamente el comportamiento que el tema 35 pedía a mano, ahora garantizado por el compilador.
¿Para qué sirve -MP?
Imagina que texto.c incluía un encabezado aux.h y se decide eliminarlo (borrando el archivo y la línea #include). El .d viejo todavía lo menciona como requisito de texto.o. Sin -MP esa referencia colgante rompe la compilación aunque el código ya esté bien; con -MP, la regla vacía le da a make una forma de «construir» el archivo que no existe y sigue adelante:
$ grep aux build/debug/texto.d
build/debug/texto.o: src/texto.c include/util.h include/aux.h
include/aux.h:
$ rm include/aux.h; sed -i '/aux.h/d' src/texto.c
$ make
CC src/texto.c
AR build/debug/libbasico.a
LINK build/debug/prog
(repetido con CPPFLAGS="-Iinclude -MMD", es decir, sin -MP)
$ make CPPFLAGS="-Iinclude -MMD"
make: *** No rule to make target 'include/aux.h', needed by 'build/debug/texto.o'. Stop.
El error sin -MP es de los más desconcertantes que da make en proyectos C, y la receta para evitarlo está en una opción del compilador. Por eso -MMD -MP se escriben siempre juntas.
3. Un directorio de compilación
Los objetos, los .d, las bibliotecas y los ejecutables van a build/. Eso exige tres cosas del Makefile. La primera, que la regla de patrón vincule dos carpetas distintas: src/numeros.c produce build/debug/numeros.o. La segunda, que la lista de objetos se calcule con una referencia con sustitución de patrón, que cambia el prefijo y la terminación a la vez. La tercera, que el directorio exista antes de compilar:
BUILD = build/$(MODE)
LIBSRC = src/numeros.c src/texto.c
LIBOBJ = $(LIBSRC:src/%.c=$(BUILD)/%.o)
$(BUILD)/%.o: src/%.c | $(BUILD)
$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<
$(BUILD):
mkdir -p $@
El símbolo | en los requisitos separa los normales de los requisitos de solo orden (order-only). Significan: «$(BUILD) debe existir antes de ejecutar esta receta, pero su fecha no cuenta para decidir si hay que rehacer el objeto». ¿Por qué importa? Porque un directorio cambia de fecha cada vez que se crea o borra un archivo dentro. Si $(BUILD) fuera un requisito normal, cada objeto nuevo haría al directorio «más reciente» que los anteriores y make los recompilaría sin motivo. Se puede comprobar con una copia del Makefile donde se quita la barra (bad.mk, con el directorio como requisito común):
$ make clean; make >/dev/null
$ make -f bad.mk -n | grep -c 'gcc.* -c '
3
$ make -n
make: Nothing to be done for 'all'.
Recién compilado todo, la versión con el directorio como requisito normal quiere recompilar los tres fuentes otra vez (la salida se resume contando las líneas de compilación); la de solo orden, correctamente, no tiene nada que hacer. Y como el patrón es $(BUILD)/%.o: src/%.c, un fuente nuevo en src/ no exige tocar la regla, solo agregarlo a la lista.
4. La biblioteca del proyecto
La biblioteca es un objetivo más del grafo: depende de sus objetos, se arma con ar y el programa depende de ella. Con $(LIB) como requisito, cualquier cambio en un módulo de la biblioteca rehace el .a y luego reenlaza el programa; y las opciones de enlace van en el orden que se vio en el tema 32 (objetos, -L/-l, y por último $(LDLIBS)):
LIB = $(BUILD)/libbasico.a
PROG = $(BUILD)/prog
$(PROG): $(MAINOBJ) $(LIB)
$(CC) $(LDFLAGS) -o $@ $(MAINOBJ) -L$(BUILD) -lbasico $(LDLIBS)
$(LIB): $(LIBOBJ)
ar rcs $@ $^
Ofrecer también la versión dinámica (tema 33) es una regla más; los objetos ya se compilan con -fPIC (está en CFLAGS para toda la construcción, lo más simple y con un costo mínimo en x86-64). El programa que la usa lleva -Wl,-rpath,'$$ORIGIN': el $$ se debe a lo visto en el tema 35 (make consume un $), y hace que el cargador busque la biblioteca junto al ejecutable:
SOLIB = $(BUILD)/libbasico.so
$(SOLIB): $(LIBOBJ)
$(CC) -shared -o $@ $^
$(BUILD)/prog_dyn: $(MAINOBJ) $(SOLIB)
$(CC) $(LDFLAGS) -o $@ $(MAINOBJ) -L$(BUILD) -Wl,-rpath,'$$ORIGIN' -lbasico $(LDLIBS)
$ make shared
SO build/debug/libbasico.so
LINK build/debug/prog_dyn
$ ./build/debug/prog_dyn
maximo=9 promedio=5.50
ecalne
$ ldd build/debug/prog_dyn | grep basico
libbasico.so => /home/ana/c/proy/build/debug/libbasico.so (0x0000795e55ba8000)
$ readelf -d build/debug/prog_dyn | grep RUNPATH
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN]
El programa se ejecutó sin LD_LIBRARY_PATH y sin instalar nada, porque el RUNPATH incrustado apunta a su propio directorio. Cuando hay una sola biblioteca y se la quiere ofrecer en las dos formas, esta es la manera habitual de tenerlas conviviendo en build/.
5. Los objetivos de siempre: .PHONY
Hay un conjunto de objetivos que casi cualquier proyecto con make ofrece, con nombres convencionales (la GNU Coding Standards los fijó hace décadas), de modo que quien llega a un proyecto nuevo sabe qué pedir. Ninguno es un archivo, así que todos van en .PHONY (tema 35):
| Objetivo | Qué hace |
|---|---|
all | Construye todo lo que el proyecto produce. Es el primero: el que se ejecuta con un make a secas. |
test | Compila y ejecuta las pruebas. |
install | Copia el resultado a su destino ($(PREFIX)). |
clean | Borra lo generado. |
help | Lista los objetivos disponibles (no es estándar, pero muy útil). |
.DEFAULT_GOAL := all
.PHONY: all shared test install clean help
all: $(PROG) ## compila el programa
.DEFAULT_GOAL fija el objetivo por omisión sin depender del orden del archivo, de modo que se pueden poner las variables y reglas auxiliares donde convenga. El objetivo test compila el ejecutable de pruebas, lo corre, y el código de salida de la prueba pasa a ser el de make, lo que lo hace utilizable en integración continua (si falla una prueba, falla el comando):
$(BUILD)/prueba: $(TESTOBJ) $(LIB)
$(CC) $(LDFLAGS) -o $@ $(TESTOBJ) -L$(BUILD) -lbasico $(LDLIBS)
test: $(BUILD)/prueba ## ejecuta las pruebas
$(BUILD)/prueba
$ make test
CC tests/prueba.c
LINK build/debug/prueba
todas las pruebas pasan
$ sed -i 's/== 9/== 10/' tests/prueba.c # se rompe una expectativa a propósito
$ make test; echo "rc=$?"
CC tests/prueba.c
LINK build/debug/prueba
FALLO: maximo(v, 4) == 10 (linea 20)
hay fallos
make: *** [Makefile:70: test] Error 1
rc=2
El objetivo install sigue dos convenciones que conviene respetar. PREFIX es el directorio base (por omisión /usr/local) y se puede cambiar al invocar: make install PREFIX=/opt/ana. DESTDIR es una raíz falsa que se antepone a todo, y su uso es de quienes empaquetan: permite instalar en un directorio de ensayo sin tocar el sistema. La orden install -D crea los directorios intermedios que falten:
PREFIX ?= /usr/local
install: $(PROG) ## instala en $(PREFIX)
install -D -m 755 $(PROG) $(DESTDIR)$(PREFIX)/bin/prog
install -D -m 644 $(LIB) $(DESTDIR)$(PREFIX)/lib/libbasico.a
install -D -m 644 include/util.h $(DESTDIR)$(PREFIX)/include/util.h
$ make install DESTDIR=stage PREFIX=/usr/local
$ find stage -type f | sort
stage/usr/local/bin/prog
stage/usr/local/include/util.h
stage/usr/local/lib/libbasico.a
$ make -n install DESTDIR=stage PREFIX=/opt/ana | head -2
install -D -m 755 build/debug/prog stage/opt/ana/bin/prog
install -D -m 644 build/debug/libbasico.a stage/opt/ana/lib/libbasico.a
Así se ve el resultado antes de instalar de verdad, y con -n se pueden revisar las órdenes de otro destino sin ejecutar nada. Y un truco para que el proyecto se explique solo: una convención de comentarios ## junto a cada objetivo, y un objetivo help que los muestra con grep y awk:
help: ## muestra esta ayuda
@grep -hE '^[a-z]+:.*##' Makefile | awk -F':.*## ' '{printf "%-8s %s\n", $$1, $$2}'
$ make help
all compila el programa
shared compila con la biblioteca dinamica
test ejecuta las pruebas
install instala en $(PREFIX)
clean borra lo generado
help muestra esta ayuda
Con clean en un directorio de compilación aparte, la limpieza es trivial y segura: rm -rf build. Es una de las ventajas de la separación: borrar lo generado no puede, por descuido de un patrón, llevarse un fuente.
6. Variantes: debug y release
Durante el desarrollo interesan las opciones de depuración (-O0 -g); para entregar, la optimización (-O2 -DNDEBUG). Cambiar a mano CFLAGS tiene un problema: make no sabe que las opciones cambiaron (las variables no son requisitos), y los objetos viejos se reutilizarían con las opciones nuevas. La solución limpia es una variable de modo y un directorio de compilación distinto para cada variante, de modo que no se pisan nunca:
MODE ?= debug
BUILD = build/$(MODE)
CFLAGS = -std=c17 -Wall -Wextra -fPIC
ifeq ($(MODE),release)
CFLAGS += -O2 -DNDEBUG
else
CFLAGS += -O0 -g
endif
ifeq (a,b) … else … endif es una condicional de make, evaluada al leer el Makefile (no al ejecutar la receta). Con MODE ?= debug, el modo por omisión es debug y se cambia desde la línea de órdenes:
$ make MODE=release
CC src/main.c
CC src/numeros.c
CC src/texto.c
AR build/release/libbasico.a
LINK build/release/prog
$ find build -maxdepth 1
build
build/release
build/debug
$ make clean; make V=1 MODE=release | head -2
mkdir -p build/release
gcc -Iinclude -MMD -MP -std=c17 -Wall -Wextra -fPIC -O2 -DNDEBUG -c -o build/release/main.o src/main.c
Las dos variantes conviven (build/debug y build/release), cada una con sus objetos y sus .d, y alternar entre ellas no recompila nada. La última orden muestra la compilación con las opciones de release (-O2 -DNDEBUG) y, de paso, que mkdir -p crea el árbol anidado en un solo paso.
7. Salida limpia y V=1
Las líneas de gcc con diez opciones tapan los mensajes que importan: las advertencias del compilador. Muchos proyectos muestran una línea corta por paso (CC src/main.c) y dejan ver el comando completo solo si se pide con V=1 (verbose). Se logra con dos variables:
ifeq ($(V),1)
Q =
say = @:
else
Q = @
say = @echo " $(1) $(2)"
endif
$(BUILD)/%.o: src/%.c | $(BUILD)
$(call say,CC,$<)
$(Q)$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<
Q (de quiet) vale @ o nada, y se antepone a las órdenes reales: con @, make no las imprime. say es una variable con parámetros que se invoca con $(call say,CC,$<); sus argumentos llegan como $(1) y $(2). Por omisión imprime la etiqueta corta; con V=1 se reduce a @: (una orden vacía y silenciosa), y se ve el comando completo:
$ touch src/texto.c; make
CC src/texto.c
AR build/debug/libbasico.a
LINK build/debug/prog
$ touch src/texto.c; make V=1
gcc -Iinclude -MMD -MP -std=c17 -Wall -Wextra -fPIC -O0 -g -c -o build/debug/texto.o src/texto.c
ar rcs build/debug/libbasico.a build/debug/numeros.o build/debug/texto.o
gcc -o build/debug/prog build/debug/main.o -Lbuild/debug -lbasico
Si algo falla, el mensaje del compilador aparece con toda claridad entre las etiquetas, y para investigar con detalle basta repetir con V=1. Para un proyecto de dos archivos es una molestia; para uno de doscientos es la diferencia entre ver una advertencia o perderla.
8. Make recursivo y archivos a medio hacer
Cuando un proyecto incluye subproyectos con su propio Makefile (una biblioteca de terceros en vendor/, por ejemplo), el Makefile principal los invoca con $(MAKE) -C directorio: -C hace que make cambie de directorio antes de empezar. Hay que escribir $(MAKE) y no make, porque así se hereda lo que se pidió a la ejecución principal (por ejemplo -j o -n):
/* rec.mk */
all:
$(MAKE) -C sub
/* sub/Makefile */
all:
@echo "dentro de sub"
$ make -f rec.mk
make -C sub
make[1]: Entering directory '/home/ana/c/proy/sub'
dentro de sub
make[1]: Leaving directory '/home/ana/c/proy/sub'
El «make[1]» indica el nivel de recursión. La técnica es práctica, pero tiene una limitación conocida: cada make ve solo su parte del grafo, y las dependencias entre subproyectos hay que ordenarlas a mano. Para un proyecto propio, un único Makefile que lo vea todo (como el de este tema) suele ser más fiable.
Otra defensa útil es .DELETE_ON_ERROR:. Si una receta crea el objetivo a medias y luego falla, queda un archivo corrupto que parece reciente: en la siguiente ejecución, make lo daría por bueno. Con esta línea en el Makefile, make borra el objetivo cuando su receta falla. Una receta que escribe con redirección y luego falla lo ilustra:
/* ambos: salida.txt: echo parcial > $@ ; false */
$ make -f de.mk; ls salida.txt
make: *** [de.mk:3: salida.txt] Error 1
salida.txt
$ make -f de2.mk; ls salida.txt
make: *** [de2.mk:4: salida.txt] Error 1
make: *** Deleting file 'salida.txt'
ls: cannot access 'salida.txt': No such file or directory
Sin la línea, el archivo parcial queda y el siguiente make creerá que el objetivo está hecho. Con ella, se elimina. (gcc ya borra por su cuenta un .o si falla, pero las recetas con redirecciones del shell no tienen ese cuidado.) Un último hábito de los proyectos reales: construir con make -j$(nproc) para usar todos los núcleos. Funciona bien cuando las dependencias están completas, que es lo que garantizan el tema 35 y los .d de este.
9. El Makefile completo
Reunidas todas las piezas, este es el Makefile del proyecto. Es el que se usó para todas las salidas de este tema.
CC = gcc
MODE ?= debug
BUILD = build/$(MODE)
PREFIX ?= /usr/local
CPPFLAGS = -Iinclude -MMD -MP
CFLAGS = -std=c17 -Wall -Wextra -fPIC
ifeq ($(MODE),release)
CFLAGS += -O2 -DNDEBUG
else
CFLAGS += -O0 -g
endif
ifeq ($(V),1)
Q =
say = @:
else
Q = @
say = @echo " $(1) $(2)"
endif
LIBSRC = src/numeros.c src/texto.c
LIBOBJ = $(LIBSRC:src/%.c=$(BUILD)/%.o)
MAINOBJ = $(BUILD)/main.o
TESTOBJ = $(BUILD)/prueba.o
DEPS = $(LIBOBJ:.o=.d) $(MAINOBJ:.o=.d) $(TESTOBJ:.o=.d)
LIB = $(BUILD)/libbasico.a
SOLIB = $(BUILD)/libbasico.so
PROG = $(BUILD)/prog
.DEFAULT_GOAL := all
.PHONY: all shared test install clean help
.DELETE_ON_ERROR:
all: $(PROG) ## compila el programa
shared: $(BUILD)/prog_dyn ## compila con la biblioteca dinamica
$(PROG): $(MAINOBJ) $(LIB)
$(call say,LINK,$@)
$(Q)$(CC) $(LDFLAGS) -o $@ $(MAINOBJ) -L$(BUILD) -lbasico $(LDLIBS)
$(BUILD)/prog_dyn: $(MAINOBJ) $(SOLIB)
$(call say,LINK,$@)
$(Q)$(CC) $(LDFLAGS) -o $@ $(MAINOBJ) -L$(BUILD) -Wl,-rpath,'$$ORIGIN' -lbasico $(LDLIBS)
$(LIB): $(LIBOBJ)
$(call say,AR,$@)
$(Q)ar rcs $@ $^
$(SOLIB): $(LIBOBJ)
$(call say,SO,$@)
$(Q)$(CC) -shared -o $@ $^
$(BUILD)/%.o: src/%.c | $(BUILD)
$(call say,CC,$<)
$(Q)$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<
$(BUILD)/prueba.o: tests/prueba.c | $(BUILD)
$(call say,CC,$<)
$(Q)$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<
$(BUILD)/prueba: $(TESTOBJ) $(LIB)
$(call say,LINK,$@)
$(Q)$(CC) $(LDFLAGS) -o $@ $(TESTOBJ) -L$(BUILD) -lbasico $(LDLIBS)
$(BUILD):
$(Q)mkdir -p $@
test: $(BUILD)/prueba ## ejecuta las pruebas
$(Q)$(BUILD)/prueba
install: $(PROG) ## instala en $(PREFIX)
$(Q)install -D -m 755 $(PROG) $(DESTDIR)$(PREFIX)/bin/prog
$(Q)install -D -m 644 $(LIB) $(DESTDIR)$(PREFIX)/lib/libbasico.a
$(Q)install -D -m 644 include/util.h $(DESTDIR)$(PREFIX)/include/util.h
clean: ## borra lo generado
$(Q)rm -rf build
help: ## muestra esta ayuda
@grep -hE '^[a-z]+:.*##' Makefile | awk -F':.*## ' '{printf "%-8s %s\n", $$1, $$2}'
-include $(DEPS)
Para crecer, bastan unos pocos cambios: un módulo nuevo se agrega a LIBSRC (y no hay que tocar ninguna regla); un ejecutable nuevo es otra regla como la de $(PROG); una opción de enlace como -lm va en LDLIBS, que se puede pasar desde afuera (make LDLIBS=-lm). La estructura es la misma que usan proyectos mucho mayores; lo que cambia es el tamaño de las listas.
Cuando un proyecto crece más allá de lo que un Makefile escrito a mano maneja con comodidad (detección de bibliotecas del sistema, varias plataformas), se recurre a generadores como CMake, Meson o Autotools, que a su vez producen Makefiles (o archivos de otras herramientas). Todos descansan sobre las ideas de estos dos temas: objetivos, requisitos, dependencias y recetas.
10. Errores frecuentes
| Error | Qué pasa | Corrección |
|---|---|---|
Usar -MMD sin -MP | Al borrar o renombrar un encabezado: No rule to make target 'include/x.h', needed by …. | Siempre -MMD -MP juntas; en caso de apuro, borrar los .d (o make clean). |
Olvidar -include $(DEPS) | Los .d se generan, pero nadie los lee: los encabezados vuelven a no ser requisitos. | Incluirlos al final del Makefile, con guion para tolerar que falten. |
| Poner el directorio de compilación como requisito normal | Se recompila todo en cada make, porque el directorio cambia de fecha. | Requisito de solo orden: | $(BUILD). |
| Generar los objetos en la carpeta de los fuentes | Se mezclan con el código, entran en el control de versiones y no se pueden tener dos variantes a la vez. | Directorio build/ aparte, uno por variante, y ignorado por git. |
Cambiar CFLAGS y esperar que make recompile | Las variables no son requisitos: se reutilizan los objetos viejos. | Un directorio por modo (build/debug, build/release) o make clean al cambiar. |
Olvidar .PHONY en all, test, install, clean | Si aparece un archivo o carpeta con ese nombre (test/ es muy común), el objetivo no corre. | Declararlos todos en .PHONY. |
install que escribe directamente en /usr/local sin DESTDIR | No se puede probar ni empaquetar sin permisos de administrador. | $(DESTDIR)$(PREFIX)/… en cada destino. |
rm -rf $(DIR)/* con la variable vacía | Se convierte en rm -rf /*. | Borrar rutas explícitas (rm -rf build) y no construirlas con variables que puedan estar vacías. |
Un test que ignora el resultado de las pruebas | La integración continua da verde con pruebas rotas. | Que el código de salida del ejecutable llegue a make; sin - delante. |
-rpath,$ORIGIN sin duplicar el signo | make expande $O y queda RIGIN en la ruta. | '$$ORIGIN' en el Makefile. |
Invocar make en lugar de $(MAKE) en una receta | Se pierden -j, -n y las variables de la ejecución principal. | $(MAKE) -C subdir. |
| Compilar tras un fallo y confiar en un archivo a medio generar | El objetivo parcial parece reciente y make lo da por bueno. | .DELETE_ON_ERROR:. |
| Dependencias entre subproyectos solo en el orden de las llamadas recursivas | Funciona en serie y falla con -j. | Un solo Makefile con el grafo completo, o dependencias explícitas entre objetivos. |
11. Resumen
- Un proyecto real separa
include/,src/,tests/y un directoriobuild/para todo lo generado, que puede borrarse entero y no se versiona. - Dependencias automáticas: compilar con
-MMD -MPhace quegccescriba un.dpor cada objeto con sus encabezados;-include $(DEPS)los carga.-MPevita errores al borrar un encabezado. - El directorio de compilación se crea con una regla propia y se pone como requisito de solo orden (
| $(BUILD)), para que su fecha no provoque recompilaciones. La regla$(BUILD)/%.o: src/%.cune las dos carpetas. - La biblioteca del proyecto es un objetivo más:
ar rcspara la estática y-sharedpara la dinámica (con-fPIC); el programa depende de ella y se enlaza con-L,-ly$(LDLIBS)al final.-Wl,-rpath,'$$ORIGIN'permite ejecutar sin instalar. - Los objetivos convencionales
all,test,install,cleanyhelpvan en.PHONY;.DEFAULT_GOALfija el principal.installrespetaPREFIXyDESTDIR;testpropaga el código de salida de las pruebas. - Variantes: una variable
MODEconifeqcambia las opciones y el directorio (build/debug,build/release), de modo que conviven sin recompilar al alternar. Qysaydan una salida de una línea por paso, yV=1muestra los comandos completos. Una línea##junto a cada objetivo alimenta unmake help.$(MAKE) -C dirinvoca subproyectos;.DELETE_ON_ERROR:borra objetivos a medio hacer;make -j$(nproc)aprovecha los núcleos si las dependencias están completas.
12. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué problema de los Makefiles escritos a mano resuelven -MMD y -include?
Que make no lee los #include. Con -MMD, gcc escribe en un .d junto a cada objeto la lista de encabezados propios que usó, en forma de regla de make; -include $(DEPS) carga esas reglas, y así un cambio en un encabezado recompila exactamente los objetos que lo incluyen.
2. ¿Qué diferencia hay entre -MD y -MMD?
-MD lista todos los encabezados, incluidos los del sistema (stdio.h…); -MMD omite los que se incluyen con <…> desde directorios del sistema. Los encabezados del sistema casi nunca cambian, y listarlos solo agranda los .d.
3. ¿Para qué sirve -MP?
Agrega al .d una regla vacía para cada encabezado. Si se borra o renombra un encabezado, el .d viejo lo sigue nombrando como requisito; sin -MP, make falla con No rule to make target; con -MP, la regla vacía permite continuar.
4. ¿Por qué se escribe -include y no include?
Porque el guion hace que make ignore los archivos que no existen. La primera vez los .d aún no se han generado (no hay objetos), y con include a secas sería un error.
5. ¿Qué significa $(BUILD)/%.o: src/%.c | $(BUILD)?
Una regla de patrón que hace cada objeto build/…/X.o a partir de src/X.c. Lo que sigue a | es un requisito de solo orden: el directorio debe existir antes de compilar, pero su fecha no cuenta para decidir si el objeto está desactualizado.
6. ¿Por qué un requisito normal sobre el directorio causaría recompilaciones innecesarias?
Porque la fecha de un directorio cambia cada vez que se crea o borra un archivo en él. Al generarse los objetos, el directorio queda más reciente que los primeros, y en el siguiente make cada objeto parece desactualizado respecto de su requisito.
7. ¿Dónde van -lbasico y $(LDLIBS) en la línea de enlace, y por qué?
Después de los objetos que las usan, al final de la línea de gcc. El enlazador recorre las bibliotecas una vez, en el orden dado, y solo resuelve símbolos pendientes (tema 32); una biblioteca antes de los objetos que la necesitan no aporta nada.
8. ¿Por qué el Makefile escribe '$$ORIGIN' con dos signos y comillas?
El doble signo hace que make entregue un solo $ al shell (tema 35), y las comillas simples impiden que el shell expanda $ORIGIN como variable. El resultado llega a gcc literalmente y el cargador lo reemplaza por el directorio del ejecutable.
9. ¿Por qué all, test, install y clean van en .PHONY?
Porque no son archivos. Sin la declaración, si existe un archivo o directorio con ese nombre (test/ es habitual), make lo considera actualizado y no ejecuta la receta.
10. ¿Qué son PREFIX y DESTDIR en un make install?
PREFIX es el directorio base donde el programa se instalará en el sistema (por omisión /usr/local). DESTDIR es una raíz provisional que se antepone a todas las rutas durante la instalación, y sirve para ensayar o empaquetar sin tocar el sistema real: make install DESTDIR=stage deja todo en stage/usr/local/….
11. ¿Cómo hace make test para fallar cuando una prueba falla?
La receta ejecuta el programa de pruebas, que devuelve un código distinto de cero si algo falló. Como make detiene el proceso y reporta Error 1 cuando una orden termina con código distinto de cero, el propio make sale con error (código 2), lo que sirve en integración continua.
12. ¿Por qué conviene un directorio de compilación distinto para debug y release?
Porque make no sabe que cambiaron las opciones (las variables no son requisitos): con un único directorio se reutilizarían objetos compilados con las opciones anteriores. Con directorios separados, cada variante tiene sus objetos y no se recompila nada al alternar entre ellas.
13. ¿Qué hacen Q, say y V=1?
Dan una salida de una línea por paso (CC src/main.c). Q vale @ (oculta la orden real) o nada; say imprime la etiqueta corta. Con V=1, Q queda vacía y say no imprime, por lo que se ven los comandos completos de gcc.
14. ¿Por qué usar $(MAKE) y no make al llamar a un subproyecto?
Porque $(MAKE) hereda las opciones y variables de la ejecución principal (-j, -n, V=1…) y coordina el paralelismo. Una llamada directa a make las pierde.
15. ¿Qué hace .DELETE_ON_ERROR: y cuándo importa?
Hace que make borre un objetivo cuya receta falló. Importa con recetas que escriben el archivo por pasos o con redirecciones: si falla a mitad, queda un archivo incompleto con fecha reciente que, sin la línea, la siguiente ejecución daría por bueno.
Para practicar
- Monta el proyecto del tema con tus propios módulos. Haz que cada fuente incluya dos encabezados distintos, toca cada uno por separado y comprueba con
make -nqué objetos se recompilan. - Quita
-MPdel Makefile, borra un encabezado incluido y reproduce el errorNo rule to make target. Restituye-MPy comprueba que se resuelve. - Quita la barra
|de la regla de los objetos y mide conmake -ncuántas compilaciones quiere hacer tras una construcción completa. Vuelve a ponerla. - Agrega un segundo programa (
src/otro.ccon su propiomain) como ejecutable adicional enlazado con la biblioteca. ¿Qué cambia enLIBSRC, en los objetos y enDEPS? - Añade un modo
MODE=asanque compile con-fsanitize=address,undefined -gy enlace con las mismas opciones (las necesita tambiénLDFLAGS). Verifica que convive condebugyrelease. - Añade un objetivo
runque ejecute el programa y otroformatolintque haga algo útil (por ejemplo,gcc -fsyntax-only -Wall -Wextra -pedanticsobre todos los fuentes). Decláralos en.PHONYy con comentario##. - Instala con
make install DESTDIR=$PWD/stage PREFIX=/usry revisa el árbol resultante. Ejecutastage/usr/bin/progy confirma que funciona (¿por qué no necesita la biblioteca instalada?). - Crea una carpeta
vendor/con un mini subproyecto con su propio Makefile y llámalo desde el principal con$(MAKE) -C. Prueba conmake -j4y conmake -n: ¿qué muestra cada nivel?