Aprende Makefile con los mejores ejemplos
(makefiletutorial.com)- Makefile es una herramienta que simplifica la automatización de builds en C/C++ y la gestión de dependencias
- Usa un método de detección de archivos modificados mediante marcas de tiempo, por lo que solo ejecuta la compilación cuando hace falta
- Explica la estructura clave como reglas (
rule), comandos (command) y dependencias (prerequisite) con ejemplos - También cubre de forma práctica funciones avanzadas como variables automáticas, reglas de patrón y expansión de variables
- Presenta la importancia de la escalabilidad y el mantenimiento mediante una plantilla práctica de Makefile para proyectos de tamaño medio
Introducción a la guía tutorial de Makefile
- Makefile es una herramienta clave para automatizar el build de proyectos y gestionar dependencias
- Aunque puede parecer compleja al principio por sus múltiples reglas implícitas y símbolos, esta guía organiza los puntos principales con ejemplos breves y ejecutables directamente
- Cada sección permite comprender el contenido a partir de ejemplos prácticos
Primeros pasos
Para qué existe Makefile
- Makefile se usa en programas grandes para recompilar solo las partes modificadas
- Además de C/C++, existen herramientas de build específicas para muchos lenguajes, pero Make se usa en escenarios generales de build
- La lógica central consiste en detectar los archivos modificados y ejecutar solo las tareas necesarias
Sistemas de build alternativos a Make
- En C/C++: hay varias opciones como SCons, CMake, Bazel y Ninja
- En Java: Ant, Maven, Gradle, etc.
- Go, Rust y TypeScript también ofrecen sus propias herramientas de build
- En lenguajes interpretados como Python, Ruby o JavaScript, no se requiere compilación, así que la necesidad de una gestión aparte como Makefile es menor
Versiones y tipos de Make
- Existen varias implementaciones de Make, pero esta guía está optimizada para GNU Make (usado principalmente en Linux y MacOS)
- Los ejemplos son compatibles tanto con GNU Make 3 como con la versión 4
Cómo ejecutar los ejemplos
- Después de instalar
makeen la terminal, guarda cada ejemplo en un archivoMakefiley ejecuta el comandomake - Las líneas de comando dentro del Makefile deben ir indentadas obligatoriamente con tabulaciones
Sintaxis básica de Makefile
Estructura de una regla (Rule)
-
objetivo: dependencia(s)- comando
- comando
-
Objetivo: nombre del archivo resultante del build (normalmente uno)
-
Comando: el script de shell que se ejecuta realmente (comienza con tabulación)
-
Dependencia: lista de archivos que deben estar listos antes de construir el objetivo
La esencia de Make
Ejemplo Hello World
hello:
echo "Hello, World"
echo "This line will print if the file hello does not exist."
- El objetivo
hellono tiene dependencias y ejecuta 2 comandos - Al ejecutar
make hello, si el archivohellono existe, se ejecutan los comandos. Si ya existe, no se ejecutan - En general, se escribe haciendo coincidir el objetivo con el nombre del archivo
Ejemplo básico de compilación de un archivo C
- Crear el archivo
blah.c(con el contenidoint main() { return 0; }) - Escribir el siguiente Makefile
blah:
cc blah.c -o blah
- Al ejecutar
make, si el objetivoblahno existe, se compila y se crea el archivoblah - Aunque
blah.ccambie después, no se recompila automáticamente → hace falta agregar una dependencia
Cómo agregar dependencias
blah: blah.c
cc blah.c -o blah
- Ahora, si
blah.ccambió recientemente, el objetivoblahse vuelve a construir - Se usan las marcas de tiempo de los archivos como criterio para detectar cambios
- Si las marcas de tiempo se manipulan manualmente, puede comportarse distinto a lo esperado
Más ejemplos
Ejemplo de objetivos y dependencias encadenados
blah: blah.o
cc blah.o -o blah
blah.o: blah.c
cc -c blah.c -o blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
- Las dependencias se siguen como una estructura en árbol y se automatiza la generación en cada etapa
Ejemplo de objetivo que siempre se ejecuta
some_file: other_file
echo "This will always run, and runs second"
touch some_file
other_file:
echo "This will always run, and runs first"
- Como
other_fileno se crea como archivo real, el comando desome_filese ejecuta cada vez
Make clean
- El objetivo
cleanse usa con frecuencia para borrar los artefactos del build - No es una palabra reservada especial en Make; hay que definirlo manualmente como comando
- Si existe un archivo llamado
clean, puede causar confusión, por lo que se recomienda usar.PHONY
Ejemplo:
some_file:
touch some_file
clean:
rm -f some_file
Manejo de variables
- Las variables siempre son cadenas de texto.
- Normalmente se recomienda
:=, aunque existen varias formas de asignación como=,?=y+= - Ejemplo de uso:
files := file1 file2
some_file: $(files)
echo "Look at this variable: " $(files)
touch some_file
file1:
touch file1
file2:
touch file2
clean:
rm -f file1 file2 some_file
- Las variables se referencian como
$(variable)o${variable} - Las comillas en un Makefile no tienen significado para Make en sí (aunque sí pueden ser necesarias en comandos de shell)
Gestión de objetivos
Objetivo all
- Para ejecutar varios objetivos a la vez, se le da ese rol al primer objetivo (el predeterminado)
all: one two three
one:
touch one
two:
touch two
three:
touch three
clean:
rm -f one two three
Múltiples objetivos y variables automáticas
- Es posible ejecutar comandos individuales para varios objetivos.
$@contiene el nombre del objetivo actual
all: f1.o f2.o
f1.o f2.o:
echo $@
Variables automáticas y comodines
Comodín *
*busca directamente nombres en el sistema de archivos- Se recomienda usarlo siempre envuelto en la función
wildcard
print: $(wildcard *.c)
ls -la $?
- No uses
*directamente en la definición de variables
thing_wrong := *.o
thing_right := $(wildcard *.o)
Comodín %
- Se usa sobre todo en reglas de patrón, donde permite extraer y expandir el patrón especificado
Reglas avanzadas
Reglas implícitas (Implicit)
- Make incluye varias reglas predeterminadas ocultas relacionadas con builds de C/C++
- Variables representativas:
CC,CXX,CFLAGS,CPPFLAGS,LDFLAGS, etc. - Ejemplo en C:
CC = gcc
CFLAGS = -g
blah: blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
clean:
rm -f blah*
Static Pattern Rules
- Permiten escribir de forma concisa varias reglas que siguen el mismo patrón
objects = foo.o bar.o all.o
all: $(objects)
$(CC) $^ -o all
$(objects): %.o: %.c
$(CC) -c $^ -o $@
all.c:
echo "int main() { return 0; }" > all.c
%.c:
touch $@
clean:
rm -f *.c *.o all
Static Pattern Rules + función filter
- Con
filter, se pueden seleccionar solo los elementos que coincidan con un patrón de extensión específico
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
all: $(obj_files)
.PHONY: all
$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $
1 comentarios
Opiniones de Hacker News
Alguien contó que en 1985 vio personalmente a una persona en el laboratorio de Graphics de Boston University crear un renderizador 3D para animación usando Makefile. Esa persona era programadora de Lisp, estaba trabajando en generación temprana de procedimientos y en un sistema de actores 3D, y creó un Makefile realmente elegante de unas 10 líneas. La estructura generaba automáticamente cientos de animaciones usando simples dependencias por fecha de archivos. Las formas 3D de cada cuadro se creaban en Lisp, y Make se encargaba de generar los cuadros. En 1985, a diferencia de hoy que damos por sentado el 3D y la animación, todo el mundo quedaba asombrado; recuerda que esa persona era Brian Gardner, quien después estuvo a cargo del renderizador 3D de Iron Giant y Coraline
Alguien expresó curiosidad sobre si se trata de la persona que aparece en 3d-consultant.com/bio.html
Preguntan si efectivamente se refería a la película Coraline
Presentan algunas banderas útiles de Make que no son tan conocidas
--output-sync=recurse -j10: significa que agrupa stdout/stderr y los imprime hasta que termina el trabajo de cada objetivo; de otro modo, los logs se mezclan y se vuelven difíciles de analizar-jse puede usar--load-averagepara controlar la carga del sistema durante el paralelismo (make -j10 --load-average=10)--shuffle, que mezcla aleatoriamente la planificación de objetivos de compilación, es útil en entornos de CI para detectar problemas de dependencias en el MakefileSe menciona la idea de recopilar formalmente las distintas opciones de make e incluirlas en texto o documentación dentro del programa para facilitar su uso
Explica que la opción que más usa es la bandera
-B, para forzar una compilación completaComo ha visto con frecuencia problemas provocados por
make -jen máquinas DOS, percibe ese fenómeno como un bugPregunta si los problemas de paralelización en sistemas ocupados o multiusuario no deberían ser responsabilidad del scheduler del OS
Aunque son banderas útiles, recomienda no usarlas fuera de proyectos privados para uno mismo porque estas opciones no son portables
Considera que saltarse
.PHONYen un tutorial con el argumento de que no se usa es una excusa débil. Opina que lo correcto es enseñar a usar bien la herramienta.PHONYen todas las recetas.PHONYpor receta o agruparlo todo una sola vez al inicio del archivo, y le gustaría que un linter lo hiciera cumplir-o pipefaila ciegas es problemático; puede romperse al usar grep y otros comandos en pipes, así que recomienda aplicarlo según el caso.PHONYes riguroso, pero casi siempre innecesario y solo vuelve más verboso el Makefile; prefiere usarlo solo cuando haga faltaAfirman que Make es una herramienta especializada en compilar grandes codebases en C
Otro opina que Make, más que un job runner, es una herramienta shell de propósito general que transforma scripts shell lineales en una forma declarativa basada en dependencias
También se plantea que ya no tiene sentido ver Make solo como una herramienta de compilación para codebases en C. Menciona que en los últimos 20 años se han desarrollado sistemas de build más robustos y claros, y que hace falta actualizar esa visión
Preguntan cuál sería un buen job runner. (Luego agrega una disculpa por haber confundido el significado de job runner)
Recomiendan just como una herramienta moderna para reemplazar las partes del Makefile que se vuelven complejas
just sirve bien como reemplazo de una lista de scripts shell, pero no puede sustituir la función esencial de Make de “ejecutar solo las reglas que necesitan volver a correrse”
Otras alternativas mencionadas son
Aunque esas herramientas alternativas se presentan como reemplazos de Make, piensa que en realidad son muy distintas y que incluso compararlas es difícil. El núcleo de Make está en generar artefactos y no recompilar lo que ya fue construido. En cambio, just cumple el papel de simple ejecutor de comandos
La ventaja de usar Make como ejecutor de comandos es la confiabilidad de ser una herramienta estándar instalada casi en todas partes. Aunque las alternativas estén mejor diseñadas, no siente la necesidad de usarlas por la carga de tener que instalarlas aparte
Usa Task en proyectos sencillos de hobby en C, pero todavía no sabe si también es adecuado para proyectos grandes (sitio oficial de Task)
Le parece interesante que recientemente CMake haya elegido ninja como predeterminado al considerar que Makefile no es adecuado para soporte de módulos de C++20 (guía de CMake)
clang-scan-deps(slides técnicas)En realidad, cree que esta limitación es una decisión del lado de CMake o un problema de falta de voluntarios para el generador de Makefiles. Señala que ninja tampoco puede soportar directamente módulos de C++ (issue relacionado), y que ninja incluso tiene menos funciones que Make y exige que todas las dependencias se especifiquen estáticamente
Opinan que la introducción misma de los módulos es compleja y confusa
Preguntan si alguien tiene experiencia usando tup. (documentación oficial)
Se presenta como el creador y maintainer principal de la herramienta alternativa a Make llamada Task. Dice que la desarrolla desde hace más de 8 años y sigue evolucionando
También recomienda just como otra alternativa a Make (GitHub de just)
Como coincidencia curiosa, comenta que usa Task con frecuencia y que incluso esta mañana abrió un issue
Este tutorial tiene problemas peligrosos y sutiles
ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))Tiene la costumbre de incluir siempre un Makefile en cada repo de GitHub
makese ejecuta de inmediato el comportamiento esperado para ese proyecto sin tener que recordarlo aparte