Entendiendo Make
August 1, 2026 · – visitantes
make aparece en casi todos los proyectos embebidos y de sistemas que he tocado — recetas de Yocto, builds de kernel, U-Boot, pequeños repositorios de firmware. También es una de esas herramientas que la gente usa durante años por imitación, copiando un Makefile del último proyecto sin llegar nunca a fijar del todo el modelo que hay detrás. Aquí está ese modelo, expuesto directamente.
¿Qué es Make?
Make es una herramienta de automatización de builds: lee un archivo (un Makefile) que describe un conjunto de objetivos (targets), los prerrequisitos de los que depende cada objetivo, y la receta (comandos de shell) necesaria para producir ese objetivo a partir de sus prerrequisitos. Dada esa descripción, make averigua qué objetivos están desactualizados y ejecuta solo las recetas necesarias para ponerlos al día — nada más.
main: main.o utils.o
gcc -o main main.o utils.o
main.o: main.c
gcc -c main.c
Ejecuta make, y construirá main.o solo si main.c es más reciente que main.o (o si main.o todavía no existe), y luego enlazará main solo si main.o o utils.o cambiaron. Nada se reconstruye innecesariamente.
¿Para quién es?
Make fue escrito por Stuart Feldman en los Bell Labs en abril de 1976. Según su propio relato, el detonante fue un colega — Steve Johnson, autor de yacc — que perdió toda una mañana depurando un programa que, en realidad, era correcto; el binario simplemente no se había vuelto a enlazar tras un cambio en el código fuente. Feldman recibió el ACM Software System Award de 2003 por ello. La herramienta que salió de esa tarde — registrar las dependencias una sola vez y dejar que la máquina averigüe qué hay que rehacer — se convirtió en una de las ideas más copiadas en herramientas de software. GNU Make, la versión que casi todo el mundo usa hoy, es una reimplementación libre del Proyecto GNU (1) (5) (6).
En la práctica, es para cualquiera que ensambla un artefacto no trivial — un binario compilado, un documento, un conjunto de archivos generados — a partir de piezas más pequeñas, donde reconstruir todo desde cero en cada cambio sería un desperdicio o demasiado lento. Eso cubre proyectos en C/C++, builds de kernel y bootloader, documentos LaTeX, y bastante automatización ad hoc que no tiene nada que ver con compilar código.
¿Cuándo deberías recurrir a él?
Make se gana su lugar cuando tres cosas son ciertas a la vez:
- Existe un grafo de dependencias real — algunas salidas dependen de otras, y reconstruir no es simplemente "regenerar todo".
- Las reconstrucciones son lo bastante costosas como para que evitar las innecesarias importe (una compilación de dos segundos no necesita esto; un build de kernel de veinte minutos sí).
- Las reglas son razonablemente estables — el modelo de Make basado en timestamps de archivos no maneja pipelines muy dinámicos con la misma gracia que un ejecutor de tareas con invalidación explícita.
Para un puñado de archivos sin interdependencias, un script de shell suele ser más simple y más honesto. Make empieza a compensar cuando "¿esto realmente necesitaba reconstruirse?" se convierte en una pregunta que vale la pena hacer automáticamente en lugar de a mano.
¿Dónde se sitúa en la cadena de herramientas?
Make normalmente se sitúa una capa por encima del compilador y una capa por debajo de lo que ejecuta un humano. En un proyecto C típico: archivos fuente → (compilador, dirigido por Make) → archivos objeto → (enlazador, dirigido por Make) → binario final. Make en sí no compila ni enlaza nada — invoca las herramientas que lo hacen, como comandos de shell en cada receta.
También tiende a situarse por debajo de herramientas de configuración de build de más alto nivel. CMake, Autotools y BitBake de Yocto, por ejemplo, no reemplazan a Make tanto como generan Makefiles (o dirigen algo equivalente) como su backend de ejecución real. Así que incluso los proyectos que nunca te muestran un Makefile directamente a menudo están ejecutando uno en algún punto del pipeline.
¿Por qué funciona como funciona?
La decisión de diseño central — comparar las horas de modificación de los archivos para decidir qué está obsoleto — es lo que hace que Make sea rápido en árboles grandes: comprobar un timestamp es barato, así que reconstruir un proyecto enorme después de un cambio de una línea puede saltarse casi todo y seguir siendo correcto, siempre que el grafo de dependencias que se le dio sea preciso.
Esa última cláusula es el origen de casi todas las quejas de "Make está roto" con las que te vas a topar, y vale la pena detenerse en ella: Make es solo tan correcto como el grafo del que se le informó. Si un archivo .c hace #include de una cabecera que no está listada como prerrequisito del archivo .o correspondiente, editar esa cabecera no disparará una reconstrucción — no porque Make esté confundido, sino porque nadie le dijo nunca que esa dependencia existía. Por eso los Makefiles del mundo real casi siempre se combinan con un paso de generación de dependencias hecho por el compilador (-MMD -MP con GCC/Clang) en lugar de listas de cabeceras escritas a mano — el compilador ya conoce el grafo real de includes, así que es la única fuente fiable para ello (4).
¿Cómo funciona realmente?
Objetivos, prerrequisitos, recetas. La unidad básica es:
target: prerequisite1 prerequisite2
recipe line 1
recipe line 2
Las líneas de receta deben ir indentadas con un tabulador, no con espacios — una famosa verruga histórica que todavía hace tropezar a la gente hoy en día.
Las variables automáticas reducen la repetición:
%.o: %.c
gcc -c $< -o $@
$@ es el objetivo, $< es el primer prerrequisito — esta única regla de patrón puede construir cada .o a partir de su .c correspondiente sin una línea por archivo.
Los objetivos .PHONY marcan nombres que no corresponden a archivos reales — clean, all, test — para que Make no se confunda si resulta que existe un archivo con ese nombre, y siempre ejecuta la receta en lugar de comprobar timestamps: (2)
.PHONY: clean
clean:
rm -f *.o main
Las variables evitan repetir flags por todas partes y facilitan la reutilización entre proyectos:
CC = gcc
CFLAGS = -Wall -O2
main.o: main.c
$(CC) $(CFLAGS) -c main.c
La generación automática de dependencias cierra la brecha descrita antes:
CFLAGS = -Wall -O2 -MMD -MP
-include $(wildcard *.d)
%.o: %.c
$(CC) $(CFLAGS) -c $<
-MMD hace que el compilador emita un archivo .d por cada objeto, listando cada cabecera realmente incluida durante esa compilación. -include incorpora esos archivos de vuelta al Makefile para que el grafo de dependencias se mantenga preciso a medida que cambian las cabeceras — sin que nadie tenga que mantenerlo a mano (3).
Reflexión final
La mayor parte de lo que parecen peculiaridades de Make — las comparaciones de timestamps, la insistencia en los tabuladores, la forma en que una dependencia faltante falla en silencio en lugar de ruidosamente — se desprende de una sola decisión de diseño tomada en 1976: mantener el modelo simple y la comprobación barata, y confiar en que el Makefile describa la realidad con precisión. Una vez que esa es la lente, la mayoría de los momentos de "Make se está comportando raro" resultan ser "el grafo que le di era incompleto" — que es algo mucho más fácil de arreglar.
Lecturas Relacionadas
- Git: borrar ramas, fusionar y deshacer un mal push — los comandos de rama y push del día a día
Referencias
- Feldman, S. I. (1979). Make — A Program for Maintaining Computer Programs. Software: Practice and Experience, 9(4), 255–265. — el propio artículo de Feldman presentando Make, incluida la motivación original.
- GNU Make Manual — la referencia completa y autorizada de la sintaxis y semántica de GNU Make.
- GNU Make Manual: Automatic Prerequisites — el patrón
-MMD/-includeexplicado por los propios mantenedores. - GCC: Preprocessor Options (
-Mfamily) — qué hacen-MMDy-MPpor dentro. - Make (software) — Wikipedia — historia general y linaje de las implementaciones de Make (BSD Make, GNU Make, NMAKE, etc.).
- Stuart Feldman — Wikipedia — contexto sobre el trabajo de Feldman en Bell Labs y el ACM Software System Award de 2003.
Comentarios