make y el grafo de dependencias de build
make no es "un shell script con tabs." Es un motor chico de grafos. Targets dependen de
prerequisites, las rules describen cómo reconstruir targets y los timestamps deciden si un
nodo está stale. Ese modelo existe porque los proyectos C están partidos en translation
units: tocar un .c no debería reconstruir el mundo, pero tocar un header compartido sí
debería reconstruir cada object que depende de él.
El reset: un
Makefilees un grafo de dependencias más recipes. El grafo decide si reconstruir; la recipe dice cómo.
Cómo funciona realmente
Una rule tiene target, prerequisites y recipe:
target: prerequisite ...
command
Cuando pedís make demo, make chequea si demo existe y si algún prerequisite es más
nuevo. Si main.o o math.o es más nuevo que demo, corre la recipe de link. Para decidir
si esos objects están al día, make chequea recursivamente sus propios prerequisites. Esto
convierte una pila de archivos en un dependency graph dirigido.
Las variables mantienen líneas de comando centralizadas. Las pattern rules evitan copiar y
pegar recipes de compile. Variables automáticas como $@ y $< significan "el target" y
"el primer prerequisite." Entonces un proyecto C mínimo puede expresar "cada .o sale de
su .c y del header compartido" en una rule.
| Idea de make | Significado |
|---|---|
| Target | archivo o nombre phony que le pedís a make construir |
| Prerequisite | archivo o target que debe estar actualizado primero |
| Recipe | comandos shell que corren cuando el target está stale |
| Pattern rule | rule reusable como %.o: %.c |
| Phony target | target tipo comando que no es un archivo real de salida |
| Timestamp | señal default de frescura |
Make no parsea C semánticamente por sí solo. Si tu object depende de un header, el
Makefile tiene que decirlo, o el compilador tiene que emitir dependency files con opciones
como -MMD -MP. Sin edges correctos, make puede decidir honestamente que el grafo está al
día mientras el programa está stale.
Artefacto ejecutable: mirá reconstruir el grafo
El ejemplo vive en
examples/toolchain-and-linking/make-and-the-build-dependency-graph/. Su Makefile
contiene una rule de link, una pattern rule de compile y un target clean:
CC ?= gcc
CFLAGS ?= -O0 -Wall -Wextra
OBJS = main.o math.o
demo: $(OBJS)
@echo "LINK $@"
$(CC) $(OBJS) -o $@
%.o: %.c math.h
@echo "CC $< -> $@"
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(OBJS) demo
El fuente C es deliberadamente chico:
#include <stdio.h>
#include "math.h"
int main(void) {
printf("make graph result: %d\n", add_bias(31));
return 0;
}
Corrélo:
cd examples/toolchain-and-linking/make-and-the-build-dependency-graph
./run.sh
Salida real de esta máquina:
== clean build ==
CC main.c -> main.o
cc -O0 -Wall -Wextra -c main.c -o main.o
CC math.c -> math.o
cc -O0 -Wall -Wextra -c math.c -o math.o
LINK demo
cc main.o math.o -o demo
== run ==
make graph result: 42
== no-op build query ==
make -q: graph is up to date
== header timestamp change rebuilds dependents ==
CC main.c -> main.o
cc -O0 -Wall -Wextra -c main.c -o main.o
CC math.c -> math.o
cc -O0 -Wall -Wextra -c math.c -o math.o
LINK demo
cc main.o math.o -o demo
El edge del header se ve. Tocar math.h vuelve stale a main.o y math.o porque la
pattern rule dice que cada object depende de ese header. Si la rule listara solo %.c,
make se perdería el cambio de header y el build podría usar object files stale en silencio.
Modos de falla y trade-offs
- Edges de dependencia faltantes. El bug clásico es cambiar un header y no reconstruir todos los objects que lo incluyen.
- Granularidad de timestamps. Cambios de archivo muy rápidos o clock skew pueden confundir chequeos por timestamp. Archivos generados empeoran esto.
- Tabs son sintaxis. Las recipes deben empezar con tab en la sintaxis tradicional de make; espacios pueden producir errores confusos.
- Targets phony pueden chocar con archivos. Si existe un archivo real llamado
cleany el target no es.PHONY,make cleanpuede no hacer nada. - Recursive make esconde edges globales. Partir en muchos sub-makes puede perder información de dependencias entre directorios.
- Make es bajo nivel. Es excelente para control directo, pero no descubre features de plataforma o dependencias de paquetes como generadores más altos.
En la práctica
- Modelá primero el grafo. Preguntá "¿qué archivo produce qué archivo, y qué inputs lo vuelven stale?"
- Usá pattern rules para edges de compile. Reducen duplicación y mantienen flags consistentes entre objects.
- Generá dependencias de headers.
-MMD -MPmás.dfiles incluidos es el patrón común de producción en C/C++. - Mantené recipes aburridas. Make debería decidir el grafo; las recipes shell deberían hacer la acción de build nombrada por el target.
- Usá make como capa de ejecución. Incluso cuando CMake u otro generador escribe los archivos de build, la idea de abajo sigue siendo un dependency graph.
Conecta con: Toolchain y Linking · Translation units, declaraciones vs definiciones y linkage · gcc vs clang y el modelo de compiler driver · Static linking y archives
Fuentes
- GNU Make Manual — Rules — targets, prerequisites, recipes y lógica de update. https://www.gnu.org/software/make/manual/make.html#Rule-Introduction
- GNU Make Manual — Pattern Rules — rules reusables estilo
%.o: %.cy variables automáticas. https://www.gnu.org/software/make/manual/make.html#Pattern-Rules - GNU Make Manual — Phony Targets — por qué targets tipo comando deberían marcarse
.PHONY. https://www.gnu.org/software/make/manual/make.html#Phony-Targets - GCC Manual — opciones de dependency generation —
-M,-MM,-MMDy flags relacionados para archivos de dependencias de headers. https://gcc.gnu.org/onlinedocs/gcc/Preprocessor-Options.html - David Drysdale — Beginner's Guide to Linkers — contexto de por qué objects separados y pasos de link crean dependencias reales de build. https://www.lurklurk.org/linkers/linkers.html