← Programación en C para Sistemas Operativos Bloque 8 · Compilación, enlazado y bibliotecas · Tema 36 de 42

Tema 36

Makefiles para proyectos reales

El tema 35 dejó un Makefile que funciona para tres archivos en un solo directorio, con una pega que quedó anunciada: los encabezados se declaran a mano, y olvidar uno produce un binario viejo sin ningún aviso. Un proyecto de verdad tiene más: carpetas para fuentes, encabezados y pruebas; una biblioteca propia; una compilación en modo depuración y otra optimizada; un lugar donde instalar; y la expectativa de que make sea rápido y de que git status no se llene de archivos generados. Este tema construye, paso a paso y con cada salida verificada, un Makefile con ese nivel: dependencias automáticas con gcc -MMD -MP, un directorio build/ separado, la biblioteca del proyecto, los objetivos .PHONY habituales (all, test, install, clean), variantes debug y release, y salida limpia. Cierra el bloque de compilación y enlazado.

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.

estructura del proyecto
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:

tests/prueba.c
#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:

Makefile · el fragmento que automatiza las dependencias
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:

terminal
$ 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:

terminal · solo se rehace lo que depende de lo modificado
$ 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:

terminal · el mismo borrado, con y sin -MP
$ 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:

Makefile · fragmento
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):

terminal · el directorio como requisito normal frente a solo de orden
$ 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)):

Makefile · fragmento
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:

Makefile · fragmento · y terminal
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):

ObjetivoQué hace
allConstruye todo lo que el proyecto produce. Es el primero: el que se ejecuta con un make a secas.
testCompila y ejecuta las pruebas.
installCopia el resultado a su destino ($(PREFIX)).
cleanBorra lo generado.
helpLista los objetivos disponibles (no es estándar, pero muy útil).
Makefile · fragmento
.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):

Makefile · fragmento · y terminal
$(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:

Makefile · fragmento · y terminal
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:

Makefile · fragmento · y terminal
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:

Makefile · fragmento
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:

terminal
$ 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:

Makefile · fragmento
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:

terminal
$ 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, sub/Makefile y terminal
/* 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:

de.mk (sin la línea) y de2.mk (con ella)
/* 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.

Makefile
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

ErrorQué pasaCorrección
Usar -MMD sin -MPAl 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 normalSe recompila todo en cada make, porque el directorio cambia de fecha.Requisito de solo orden: | $(BUILD).
Generar los objetos en la carpeta de los fuentesSe 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 recompileLas 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, cleanSi 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 DESTDIRNo se puede probar ni empaquetar sin permisos de administrador.$(DESTDIR)$(PREFIX)/… en cada destino.
rm -rf $(DIR)/* con la variable vacíaSe 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 pruebasLa 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 signomake expande $O y queda RIGIN en la ruta.'$$ORIGIN' en el Makefile.
Invocar make en lugar de $(MAKE) en una recetaSe 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 generarEl objetivo parcial parece reciente y make lo da por bueno..DELETE_ON_ERROR:.
Dependencias entre subproyectos solo en el orden de las llamadas recursivasFunciona en serie y falla con -j.Un solo Makefile con el grafo completo, o dependencias explícitas entre objetivos.

11. Resumen

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

  1. 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 -n qué objetos se recompilan.
  2. Quita -MP del Makefile, borra un encabezado incluido y reproduce el error No rule to make target. Restituye -MP y comprueba que se resuelve.
  3. Quita la barra | de la regla de los objetos y mide con make -n cuántas compilaciones quiere hacer tras una construcción completa. Vuelve a ponerla.
  4. Agrega un segundo programa (src/otro.c con su propio main) como ejecutable adicional enlazado con la biblioteca. ¿Qué cambia en LIBSRC, en los objetos y en DEPS?
  5. Añade un modo MODE=asan que compile con -fsanitize=address,undefined -g y enlace con las mismas opciones (las necesita también LDFLAGS). Verifica que convive con debug y release.
  6. Añade un objetivo run que ejecute el programa y otro format o lint que haga algo útil (por ejemplo, gcc -fsyntax-only -Wall -Wextra -pedantic sobre todos los fuentes). Decláralos en .PHONY y con comentario ##.
  7. Instala con make install DESTDIR=$PWD/stage PREFIX=/usr y revisa el árbol resultante. Ejecuta stage/usr/bin/prog y confirma que funciona (¿por qué no necesita la biblioteca instalada?).
  8. Crea una carpeta vendor/ con un mini subproyecto con su propio Makefile y llámalo desde el principal con $(MAKE) -C. Prueba con make -j4 y con make -n: ¿qué muestra cada nivel?