Reglas de patrón y variables
Los dos sabores de asignación, las variables automáticas, las reglas de patrón y las implícitas heredadas de los años setenta. Con todo ello, un Makefile genérico de veinte líneas que no vuelves a escribir nunca más.
Un Makefile explícito es honesto y es insostenible: tres reglas por cada fuente, y toda la información duplicada. Las reglas de patrón y las variables automáticas existen para colapsar esa repetición en una única plantilla que Make instancia a demanda. El resultado no es solo más corto: es un fichero que deja de mentir cuando el proyecto crece, porque ya no hay nada que actualizar a mano.
- Distinguir la asignación diferida de la inmediata y predecir el momento exacto de cada expansión.
- Usar las variables automáticas para escribir recetas que no repitan ningún nombre de fichero.
- Reconocer las reglas implícitas heredadas y decidir conscientemente si desactivarlas.
- Construir un Makefile genérico que descubra sus propias fuentes y sirva para cualquier proyecto.
Dos sabores de asignación
Make tiene dos operadores de asignación que se comportan de forma radicalmente distinta, y elegir mal es una fuente clásica de builds lentos e incomprensibles.
CC := gcc # inmediata: se evalua aqui, una vez
CFLAGS := -std=c23 -Wall -Wextra -O2 -g
LDFLAGS = -L$(PREFIX)/lib # diferida: se evalua en cada uso
PREFIX ?= /usr/local # solo si aun no tiene valor
CFLAGS += -Wconversion -Wshadow # anade al final
VERSION != git describe --tags # asigna la salida de un comando
La asignación con := es inmediata: el lado derecho se expande en el momento de leer la línea y el resultado queda congelado. La asignación con = es diferida: el texto se guarda sin expandir y se reevalúa cada vez que la variable aparece en una receta.
La diferencia se vuelve visible con funciones costosas. Si escribes SRC = $(shell find . -name '*.c') con asignación diferida, ese find se ejecuta de nuevo en cada expansión, potencialmente decenas de veces por invocación. Con := se ejecuta una sola vez. La regla práctica es simple: usa := por defecto y reserva = para cuando necesites de verdad que el valor dependa de algo que aún no está definido.
Las variables admiten además un ámbito local a un destino, lo que permite compilar una parte del proyecto con banderas distintas sin duplicar ninguna regla:
build/legacy.o: CFLAGS := $(CFLAGS) -Wno-conversion
debug: CFLAGS += -fsanitize=address,undefined -Og
Ese ámbito se hereda por todo el subgrafo: cualquier objetivo construido como prerrequisito de debug verá las banderas modificadas. Es potente y es peligroso, porque el mismo objeto puede acabar compilado de dos formas distintas según qué objetivo lo pidiera, y Make no guarda memoria de con qué banderas construyó nada.
Dentro de una receta, Make expande sus propias variables antes de entregar la línea a la shell. Para que una variable llegue viva a la shell hay que escaparla duplicando el dólar. Por eso los bucles de shell dentro de recetas usan la forma doble para sus propias variables mientras las de Make van con dólar simple.
Variables automáticas
Make define, dentro de cada receta, un conjunto de variables que describen la regla que se está ejecutando. Son el mecanismo que permite escribir una receta sin nombrar ningún fichero.
| Variable | Qué contiene |
|---|---|
$@ |
el objetivo de la regla |
$< |
el primer prerrequisito |
$^ |
todos los prerrequisitos, sin duplicados |
$+ |
todos los prerrequisitos, conservando duplicados y orden |
$? |
solo los prerrequisitos más recientes que el objetivo |
$* |
la parte que casó con el comodín en una regla de patrón |
$(@D) y $(@F) |
el directorio y el nombre base del objetivo |
$? es la menos conocida y la más interesante conceptualmente: contiene exactamente el subconjunto que provocó la reconstrucción. Es la vía para recetas incrementales de verdad, como actualizar una biblioteca estática añadiendo solo los objetos que cambiaron.
$(@D) resuelve el problema de construir fuera del árbol de fuentes: si el objetivo vive en un directorio que aún no existe, la receta puede crearlo antes de compilar.
Reglas de patrón e implícitas
Una regla de patrón usa el comodín para describir una familia entera de reglas. Make la instancia cuando necesita un objetivo que case con el patrón del objetivo y puede satisfacer el patrón de los prerrequisitos.
build/%.o: src/%.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -c $< -o $@
%.a:
$(AR) rcs $@ $?
Cuando Make necesita build/parser.o, prueba el patrón, deduce que la parte comodín vale parser, comprueba si src/parser.c existe o puede construirse y, si es así, instancia la regla. Ese proceso de deducción es encadenable: Make puede construir una cadena de reglas de patrón para llegar de un fichero a otro pasando por objetivos intermedios que borrará al terminar.
flowchart LR A[src slash x.c] --> B[regla de patron] B --> C[build slash x.o] C --> D[regla de enlazado] D --> E[prog] V[variables automaticas] -.instancia.-> B V -.instancia.-> D style B fill:#89b4fa,color:#11111b style D fill:#89b4fa,color:#11111b style E fill:#a6e3a1,color:#11111b
Make trae además un catálogo de reglas implícitas predefinidas: sabe convertir un fichero fuente en objeto, un objeto en ejecutable, y también gestionar Fortran, Pascal, Modula-2, ficheros RCS y SCCS. Ese catálogo es un fósil de los años setenta que sigue activo por compatibilidad, y tiene dos costes reales. El primero es de rendimiento: en cada objetivo que no encuentra regla explícita, Make prueba decenas de patrones contra el sistema de ficheros. El segundo es de diagnóstico: cuando falta una regla tuya, una implícita puede tomar el relevo en silencio y compilar con banderas que no elegiste.
make -r # desactiva las reglas implicitas predefinidas
make -R # desactiva ademas las variables implicitas
make -p | less # muestra el catalogo completo antes de desactivarlo
En un Makefile serio, MAKEFLAGS += -r en la primera línea y .SUFFIXES: vacío eliminan el fósil por completo. A partir de ahí, todo lo que ocurre está escrito por ti.
Mención aparte merece VPATH y su directiva homónima, que declaran directorios adicionales donde buscar prerrequisitos ausentes del directorio actual. Resuelven el caso real de construir fuera del árbol de fuentes, pero introducen ambigüedad en cuanto dos directorios contienen ficheros del mismo nombre, y la resolución depende del orden de la lista. El estilo moderno prefiere rutas explícitas dentro del propio patrón, que es lo que hace la plantilla de la sección siguiente.
El Makefile genérico
Con las tres piezas anteriores se construye una plantilla que descubre sus propias fuentes y no necesita mantenimiento cuando el proyecto crece.
MAKEFLAGS += -r
.SUFFIXES:
CC := gcc
CFLAGS := -std=c23 -Wall -Wextra -Wpedantic -Og -g3
LDLIBS := -lm
BIN := build/prog
SRC := $(shell find src -name '*.c')
OBJ := $(patsubst src/%.c,build/%.o,$(SRC))
.PHONY: all clean
all: $(BIN)
$(BIN): $(OBJ)
@mkdir -p $(@D)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
build/%.o: src/%.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -c $< -o $@
clean:
$(RM) -r build
patsubst traduce la lista de fuentes en la lista de objetos aplicando una sustitución de patrón, de modo que el conjunto de objetos siempre se deriva del conjunto de fuentes y nunca puede desincronizarse. Las funciones wildcard, filter, subst, notdir y foreach completan el pequeño lenguaje funcional de Make sobre listas separadas por espacios.
Este Makefile todavía tiene un agujero grave: no sabe nada de las cabeceras, así que modificar una no reconstruye nada. Ese es el asunto de la lección siguiente.
El salto conceptual de esta lección no es sintáctico. Un Makefile explícito enumera hechos: este objeto sale de esta fuente. Un Makefile con reglas de patrón declara una función de un espacio de nombres a otro —de src con extensión de fuente a build con extensión de objeto— y deja que Make la aplique donde haga falta. La lista de fuentes deja de ser un dato mantenido a mano y pasa a ser una consulta al sistema de ficheros; la lista de objetos deja de existir como dato y pasa a ser la imagen de las fuentes bajo esa función. Lo que has hecho es sustituir una enumeración por una intensión, exactamente el mismo movimiento que separa escribir cien casos de un switch de escribir una tabla y un bucle. Y el criterio de calidad se vuelve verificable: si añadir un fichero al proyecto exige tocar el Makefile, el Makefile enumera cuando debería definir. Toda la sofisticación posterior de CMake, Meson o Bazel es refinamiento de ese mismo principio; lo que cambia es cuántos detalles del sistema anfitrión te obligan a escribir a mano.
- Toma el Makefile explícito de la lección anterior y redúcelo a una sola regla de patrón sin perder funcionalidad.
- Sustituye una asignación
:=que invoque afindpor su versión con=y mide conmake -dcuántas veces se ejecuta el comando. - Escribe una regla que construya en
builddesdesrccreando los subdirectorios necesarios con$(@D). - Ejecuta
make -pen un directorio vacío, cuenta las reglas implícitas y después desactívalas con-r. Compara los tiempos. - Añade un objetivo que use
$?para actualizar una biblioteca estática solo con los objetos modificados.