¿Qué es un Makefile? Tutorial Práctico de Make con Ejemplos
Introducción
Todo proyecto tiene un puñado de comandos que ejecutas una y otra vez: compilarlo, ejecutar los tests, pasar el linter, limpiar.
Cada proyecto los escribe de forma un poco distinta, con sus propios flags y su propio orden, y recordarlos es una lata para ti y para cualquiera que se una al proyecto después.
Un Makefile los deja por escrito una sola vez, junto al código, y le da a cada uno un nombre corto: make build, make test, make lint.
Make es una de las herramientas de build más antiguas que tenemos los desarrolladores, y sigue estando en todas partes. Encontrarás Makefiles en proyectos de C, pero también en proyectos de Go, Kubernetes, Java y documentación.
En este tutorial sobre Makefile, te explico qué es un Makefile y cómo escribir uno, desde tu primera regla hasta un task runner autodocumentado para un proyecto en Go. Cada ejemplo muestra la salida que imprime de verdad en GNU Make 4.4.1, junto con las diferencias en el GNU Make 3.81 que macOS todavía trae, entre ellas los mensajes de error que es más probable que busques.
¿Qué es un Makefile?
Un Makefile es un archivo de reglas que el programa make lee para generar archivos y ejecutar tareas.
Cada regla indica un target (objetivo), sus prerequisites (prerrequisitos) y una recipe (receta): los comandos de shell que crean o actualizan el target.
Make solo ejecuta una recipe cuando su target está desactualizado.
Nada de esto es específico de C.
Un target puede ser un binario compilado, un documento generado o simplemente el nombre de una tarea como test, y una recipe puede ejecutar cualquier comando que pueda ejecutar tu shell.
Para usar un Makefile, ejecuta el comando make en el directorio que lo contiene.
Sin argumentos, Make genera el default goal (el target por defecto): el primer target del archivo, sin contar los targets especiales que empiezan por un punto, como .PHONY.
make <target> genera el que indiques, y puedes indicar varios a la vez: make clean build.
Make busca el archivo con tres nombres, en este orden: GNUmakefile, makefile y Makefile.
El manual de GNU Make recomienda Makefile, que aparece cerca de otros archivos importantes como README al listar un directorio.
Para leer un archivo con cualquier otro nombre, pásalo con -f, como en make -f build.mk.
Stuart Feldman escribió make en los Bell Labs a mediados de los años 70.
Su artículo de 1979, Make: a program for maintaining computer programs, dice que "Make se utiliza en sistemas UNIX desde 1975", mientras que el historial de revisiones del código fuente de la Seventh Edition de Unix empieza en "2.1 4/24/76 Base version".
En 2003, Feldman recibió por él el ACM Software System Award, con una mención que sigue siendo cierta: "probablemente no exista hoy en el mundo ningún sistema de software grande que no haya sido procesado por una versión o un descendiente de MAKE".
El make de Linux y macOS es GNU Make, y a fecha de octubre de 2026 su última versión sigue siendo la 4.4.1, de febrero de 2023.
Cómo crear tu primer Makefile
Crea un archivo llamado Makefile con una sola regla:
hello:
echo "Hello, Make"La segunda línea empieza con un carácter de tabulación (tab), no con espacios (más sobre esto en la siguiente sección).
Ahora ejecuta make en el mismo directorio:
$ make
echo "Hello, Make"
Hello, MakeMake imprime cada línea de la recipe antes de ejecutarla, así que primero ves el comando echo y después su salida.
Todas las reglas siguen la misma estructura:
target: prerequisites
recipe- El target es lo que produce la regla, normalmente un nombre de archivo o el nombre de una tarea, seguido de dos puntos.
- Los prerequisites son los archivos u otros targets que deben existir, y estar actualizados, antes de que se ejecute la recipe. Son opcionales:
hellono tiene ninguno. - La recipe está formada por uno o más comandos de shell, cada uno en su propia línea que empieza con una tabulación.
La introducción a las reglas del manual de GNU Make describe estas mismas tres partes y advierte sobre esa tabulación: "es un detalle oscuro que pilla a los incautos".
Tabulaciones y el error missing separator
Tarde o temprano, un editor convertirá la tabulación de una recipe en espacios, y Make se negará a leer el archivo. Con ocho o más espacios en lugar de una tabulación, GNU Make incluso adivina lo que ha pasado, una pista que da desde la versión 3.78, de 1999:
Makefile:2: *** missing separator (did you mean TAB instead of 8 spaces?). Stop.Con menos espacios, obtienes el mensaje a secas:
Makefile:2: *** missing separator. Stop.Ambos mensajes son iguales en GNU Make 4.4.1 y en el make 3.81 de macOS. El número tras el nombre del archivo es la línea que debes corregir: sustituye sus espacios iniciales por una única tabulación.
Hay un caso más traicionero.
Si la línea indentada con espacios contiene dos puntos, como @echo "Usage: make hello", Make la interpreta como una regla nueva, no da ningún error e imprime make: Nothing to be done for 'hello'.
Consejo
Para ver qué líneas empiezan con una tabulación, ejecuta cat -et Makefile: muestra cada tabulación como ^I y el final de cada línea como $, tanto en macOS como en Linux.
cat -A de GNU, en Linux, hace lo mismo, pero el cat de macOS responde cat: illegal option -- A.
En el primer Makefile, la línea de la recipe empieza con ^I, la tabulación:
$ cat -et Makefile
hello:$
^Iecho "Hello, Make"$Para que no vuelva a pasar, dile a tu editor que los Makefiles usan tabulaciones. La mayoría de editores e IDEs leen un archivo EditorConfig, de forma nativa o mediante un plugin:
[{Makefile,*.mk}]
indent_style = tabSi prefieres no usar tabulaciones en absoluto, GNU Make 3.82 y posteriores te permiten elegir otro prefijo para las recipes con .RECIPEPREFIX:
.RECIPEPREFIX = >
hello:
> echo "Hello, Make"Esto funciona en GNU Make 4.4.1, pero el make 3.81 de macOS es anterior y se detiene con Makefile:3: *** missing separator. Stop.
Yo mantendría la tabulación en cualquier Makefile que alguien pueda ejecutar en un Mac.
¿Por qué una tabulación, de entre todos los caracteres? Feldman lo explicó años después, en una cita que Eric S. Raymond incluye en The Art of Unix Programming:
"¿Por qué la tabulación en la columna 1? Yacc era nuevo, Lex era nuevísimo. No había probado ninguno de los dos, así que pensé que sería una buena excusa para aprender. Después de hacerme un lío en mi primer intento con Lex, hice algo sencillo con el patrón salto de línea y tabulación. Funcionó, y se quedó. Y unas semanas después tenía una población de unos doce usuarios, la mayoría amigos, y no quería estropear mi base instalada. El resto, por desgracia, es historia".
Prerequisites y builds incrementales
El verdadero trabajo de Make es regenerar solo lo que ha cambiado.
Esta regla crea greeting.txt a partir de name.txt:
greeting.txt: name.txt
sed 's/^/Hello, /' name.txt > greeting.txtCrea name.txt con echo Marc > name.txt y ejecuta make.
greeting.txt todavía no existe, así que Make ejecuta la recipe:
$ make
sed 's/^/Hello, /' name.txt > greeting.txt
$ cat greeting.txt
Hello, MarcEjecuta make de nuevo y, como greeting.txt ya es más reciente que name.txt, no hay nada que hacer:
$ make
make: 'greeting.txt' is up to date.En el make 3.81 de macOS, el mensaje entrecomilla el nombre de otra forma: make: `greeting.txt' is up to date.
Actualiza el prerequisite y el target vuelve a estar desactualizado:
$ touch name.txt
$ make
sed 's/^/Hello, /' name.txt > greeting.txtMake decide comparando las fechas de modificación: un target está desactualizado cuando no existe, o cuando alguno de sus prerequisites es más reciente que él.
Si automatizas estos pasos en un script, espera un segundo antes del touch: Make solo ve un cambio cuando la fecha del prerequisite es posterior a la del target, y el make de macOS las compara en segundos enteros.
Los prerequisites pueden ser otros targets, y ahí es donde Make brilla.
Añade, encima de la primera, una regla que genere shout.txt a partir de greeting.txt:
shout.txt: greeting.txt
tr a-z A-Z < greeting.txt > shout.txt
greeting.txt: name.txt
sed 's/^/Hello, /' name.txt > greeting.txtAhora make genera shout.txt, el nuevo primer target, y un cambio en name.txt regenera ambos archivos, en orden:
$ touch name.txt
$ make
sed 's/^/Hello, /' name.txt > greeting.txt
tr a-z A-Z < greeting.txt > shout.txt
$ cat shout.txt
HELLO, MARCUn cambio solo en greeting.txt regenera únicamente shout.txt.
En un proyecto real, los targets son archivos objeto, binarios o código generado, y Make recorre la misma cadena de prerequisites para regenerar solo lo que se ve afectado por un cambio.
Puedes elegir otro target por defecto con .DEFAULT_GOAL, como hace el task runner al final de este post.
Targets phony
Make asume que cada target es un archivo. Eso es un problema para los targets que nombran una tarea, no un archivo:
docs:
@echo "Generating the docs"La @ al principio de la línea de la recipe oculta el comando, así que solo se ve su salida.
Si el proyecto también tiene un directorio docs, Make lo toma como el target (para Make, un directorio también es un archivo).
La regla no tiene prerequisites que puedan dejarlo desactualizado, así que Make no ejecuta nada:
$ mkdir docs
$ make docs
make: 'docs' is up to date.Para eso sirve .PHONY: declara el target como phony (ficticio) y Make ejecutará su recipe sin importar qué archivos existan:
.PHONY: docs
docs:
@echo "Generating the docs"$ make docs
Generating the docsLa misma trampa afecta a un target test en cuanto alguien crea un archivo llamado test, o a un target build en un proyecto con un directorio build.
La sección del manual de GNU Make sobre targets phony añade un segundo motivo para usarlos: Make no busca una regla predefinida (implícita) para generar un target phony, lo que le ahorra algo de trabajo.
Todo target que da nombre a una tarea en lugar de a un archivo debe ir en .PHONY, y por eso un Makefile que hace de task runner declara phony todos sus targets.
Variables
Las variables mantienen los valores en un solo sitio, y GNU Make tiene dos tipos (flavors) principales. La diferencia está en cuándo se expande el valor:
LAZY = Hello, $(NAME)
EAGER := Hello, $(NAME)
NAME = Make
show:
@echo "lazy=[$(LAZY)] eager=[$(EAGER)]"$ make
lazy=[Hello, Make] eager=[Hello, ]=define una variable de expansión recursiva. Make guardaHello, $(NAME)tal cual y lo expande cada vez que se usa la variable, y para entoncesNAMEya está definida.:=define una variable de expansión simple. Make expande el valor una sola vez, donde se define, y en ese puntoNAMEtodavía estaba vacía.
Usa preferiblemente := para un valor calculado con $(shell ...), la función que ejecuta un comando y devuelve su salida, de modo que el comando se ejecute una sola vez en lugar de cada vez que se usa la variable.
La página del manual de GNU Make sobre los tipos de variables cubre los menos habituales.
Dos operadores más cubren casi todo lo demás.
?= establece un valor por defecto: asigna la variable solo si todavía no está definida.
NAME ?= World
hello:
@echo "Hello, $(NAME)"$ make
Hello, World
$ make NAME=Marc
Hello, Marc
$ NAME=Env make
Hello, EnvUn valor en la línea de comandos, como en make NAME=Marc, siempre sobrescribe la variable.
Una variable de entorno también la sobrescribe, pero solo porque ?= establece un valor por defecto.
Cambia la primera línea por una asignación normal y el Makefile se impone al entorno:
NAME = World
hello:
@echo "Hello, $(NAME)"$ NAME=Env make
Hello, World
$ NAME=Env make -e
Hello, Env
$ make NAME=Marc
Hello, MarcLa opción -e da prioridad al entorno, y la línea de comandos sigue imponiéndose a ambos.
+= añade un valor a una variable, separado por un espacio:
TEST_FLAGS = -v
TEST_FLAGS += -race
test:
@echo "go test $(TEST_FLAGS) ./..."$ make
go test -v -race ./...Condicionales
Make también tiene condicionales, que evalúa mientras lee el Makefile, antes de ejecutar ninguna recipe.
ifeq compara dos valores, en este caso la salida de uname -s, y else cubre el resto de casos:
ifeq ($(shell uname -s),Darwin)
OPEN := open
else
OPEN := xdg-open
endifEn macOS, $(OPEN) vale open, y en Linux xdg-open, así que una recipe puede abrir un informe generado con $(OPEN) coverage.html en ambos.
ifdef solo comprueba si una variable tiene valor.
Un uso habitual es un build silencioso: define Q := @ salvo que VERBOSE esté definida, empieza cada línea de recipe con $(Q), y make VERBOSE=1 muestra los comandos.
Cualquier valor cuenta, así que make VERBOSE=0 también los muestra.
Variables automáticas y reglas de patrón
Mientras ejecuta una recipe, Make define unas cuantas variables automáticas que describen la regla, para que no tengas que repetir sus nombres de archivo:
build/page.txt: header.txt body.txt
mkdir -p $(@D)
cat $^ > $@
@echo "target=$@ first=$< all=$^ newer=$?"Con un header.txt y un body.txt creados, la primera ejecución imprime:
$ make
mkdir -p build
cat header.txt body.txt > build/page.txt
target=build/page.txt first=header.txt all=header.txt body.txt newer=header.txt body.txtEl target no existía, así que ambos prerequisites cuentan como más recientes.
Después de touch body.txt, solo cuenta el que ha cambiado:
$ touch body.txt
$ make
mkdir -p build
cat header.txt body.txt > build/page.txt
target=build/page.txt first=header.txt all=header.txt body.txt newer=body.txt| Variable | Contiene | En el ejemplo |
|---|---|---|
$@ | El target | build/page.txt |
$< | El primer prerequisite | header.txt |
$^ | Todos los prerequisites, sin duplicados | header.txt body.txt |
$? | Los prerequisites más recientes que el target | Ambos en la primera ejecución, body.txt después de touch body.txt |
$(@D) | La parte de directorio del target | build |
El manual de GNU Make enumera todas las variables automáticas, pero estas cinco cubren la mayoría de Makefiles.
Reglas de patrón
Una regla de patrón usa % para encajar con cualquier parte de un nombre de archivo, de modo que una sola regla cubre todos los archivos de un tipo:
%.upper: %.txt
tr a-z A-Z < $< > $@$ echo hello > a.txt
$ make a.upper
tr a-z A-Z < a.txt > a.upper
$ cat a.upper
HELLO
$ make b.upper
make: *** No rule to make target 'b.upper'. Stop.Para make a.upper, el % encaja con a, así que el prerequisite es a.txt, $< es a.txt y $@ es a.upper.
No hay ningún b.txt, así que ninguna regla puede generar b.upper, y Make se detiene con make: *** No rule to make target 'b.upper'. Stop.
Así es como un Makefile de C compila cada archivo .c en un archivo .o con una sola regla %.o: %.c.
Cómo se ejecutan las recipes
Make ejecuta cada línea de una recipe en su propia shell, así que un cd en una línea se pierde en la siguiente:
where:
cd /
pwd
cd / && pwd$ make
cd /
pwd
/home/marc/project
cd / && pwd
/El primer pwd imprime el directorio en el que ejecutaste make, no /.
Para ejecutar varios comandos en la misma shell, únelos en una sola línea con &&, que además se detiene en el primer comando que falle.
Cuando esa línea se hace larga, termina cada parte con una \, y Make pasa las líneas unidas a una sola shell:
where:
cd / && \
pwdEste pwd imprime /.
La página del manual de GNU Make sobre la ejecución de recipes explica los detalles.
GNU Make 3.82 y posteriores también pueden ejecutar toda la recipe en una sola shell, con el target especial .ONESHELL:
.ONESHELL:
where:
cd /
pwdEn GNU Make 4.4.1, este pwd imprime /, mientras que el make 3.81 de macOS ignora .ONESHELL sin avisar e imprime de nuevo el directorio en el que ejecutaste make.
Pero ojo: con .ONESHELL, Make solo detecta un fallo en la última línea de la recipe, salvo que también añadas .SHELLFLAGS += -e.
Antes de pasar una línea de recipe a la shell, Make expande sus variables.
Por eso, un $ destinado a la shell debe escribirse como $$:
home:
@echo "Make variable: $(HOME)"
@echo "Shell variable: $$HOME"
@for f in a b; do echo "Item $$f"; done$ make
Make variable: /home/marc
Shell variable: /home/marc
Item a
Item b$(HOME) funciona porque Make convierte cada variable de entorno en una variable de Make, y $$HOME llega a la shell como $HOME.
Una variable de bucle solo existe en la shell, así que siempre necesita el doble dólar: con un solo $, Make expandiría $f (una variable de Make llamada f) a una cadena vacía.
Dos prefijos cambian cómo trata Make una línea de recipe.
@ oculta el comando, como en el target docs, y - ignora su fallo:
check:
-false
@echo "Still running"
false
@echo "Never printed"$ make
false
make: [Makefile:2: check] Error 1 (ignored)
Still running
false
make: *** [Makefile:4: check] Error 1Make informa del fallo en la línea 2 y continúa.
El fallo en la línea 4 lo detiene, así que la última línea nunca se ejecuta, y make termina con el código de salida 2.
El make 3.81 de macOS imprime los mismos mensajes sin el nombre de archivo ni el número de línea, make: [check] Error 1 (ignored) y make: *** [check] Error 1: los añadió GNU Make 4.0.
Opciones de línea de comandos
Estas son las opciones de línea de comandos que más uso:
| Opción | Qué hace |
|---|---|
-n | Imprime los comandos que ejecutaría, sin ejecutarlos (ejecución en seco o dry run) |
-j N | Ejecuta hasta N recipes a la vez; -j sin número no pone límite |
-C dir | Cambia a dir antes de leer el Makefile |
-f file | Lee file en lugar de GNUmakefile, makefile o Makefile |
-B | Regenera todos los targets, incluso los que están actualizados |
VAR=value | Define una variable que prevalece sobre el Makefile y el entorno |
-e | Permite que las variables de entorno sobrescriban los valores del propio Makefile |
Una ejecución en seco es una forma rápida de ver qué hace un target antes de ejecutarlo, e incluso imprime los comandos que @ oculta:
build:
@echo "Building"
go build -o hello .$ make -n build
echo "Building"
go build -o hello .Eso sí, no está totalmente libre de efectos secundarios: Make sigue ejecutando cada $(shell ...) del Makefile mientras lo lee.
-j ejecuta en paralelo los targets independientes, así que su salida puede llegar en otro orden.
Con un target slow que espera un segundo y uno fast:
all: slow fast
slow:
@sleep 1; echo "slow done"
fast:
@echo "fast done"$ make
slow done
fast done
$ make -j2
fast done
slow done-C ejecuta el Makefile de otro directorio sin salir del actual.
Con el primer Makefile en un directorio hello, GNU Make 4.4.1 imprime el directorio en el que entra y del que sale:
$ make -C hello
make: Entering directory '/home/marc/hello'
echo "Hello, Make"
Hello, Make
make: Leaving directory '/home/marc/hello'El make de macOS no imprime ninguna de las dos líneas salvo que añadas -w.
Make como task runner
En muchos proyectos, Make no genera nada por sí mismo.
Da nombres cortos a los comandos del proyecto, para que todo el mundo, y cada pipeline de CI, los ejecute de la misma forma.
Este es un Makefile para un proyecto en Go, con un target help autodocumentado:
.DEFAULT_GOAL := help
BINARY ?= hello
.PHONY: help build test clean
help: ## Show this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9\/\.-]+:.*?##/ { printf " \033[36m%-21s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
build: ## Build the binary
go build -o $(BINARY) .
test: ## Run the tests
go test ./...
clean: ## Remove the binary
rm -f $(BINARY)Lo ejecuté en un módulo Go llamado marcnuri.com/hello, con un main.go y un único test.
Si partes de cero, consulta cómo inicializar un proyecto en Go con módulos.
Un simple make imprime la ayuda, en color en una terminal (aquí sin los códigos de color):
$ make
Usage:
make <target>
help Show this help
build Build the binary
test Run the tests
clean Remove the binaryEl resto de targets ejecutan los comandos de Go:
$ make build
go build -o hello .
$ ./hello
Hello, Make$ make test
go test ./...
ok marcnuri.com/hello 0.001s$ make BINARY=greeter build
go build -o greeter .
$ make clean
rm -f helloCada parte del Makefile usa algo de las secciones anteriores:
.DEFAULT_GOAL := helphace que un simplemakeimprima la ayuda en lugar de generar el primer target. Está disponible desde GNU Make 3.81, así que también funciona en macOS.BINARY ?= helloes un valor por defecto que la línea de comandos sobrescribe, como muestramake BINARY=greeter build..PHONYincluye todos los targets, porque ninguno produce un archivo con su propio nombre.- El comentario
## ...tras cada target es su descripción. La recipe dehelpejecutaawksobre$(MAKEFILE_LIST), la lista de todos los Makefiles que Make ha leído, e imprime cada líneatarget: ## descriptionque encuentra. Una línea que empieza por##@, como##@ Build, imprime un encabezado sobre los targets que la siguen, para agrupar los targets de un Makefile más grande.
El programa awk es el de Chaos Mesh (Apache-2.0): adapta el target help agrupado con ##@ de Well documented Makefiles (2017), de Suvash Thapaliya, y además acepta / y . en los nombres de los targets.
También encontrarás el patrón en su forma más copiada, el Self-Documented Makefile de marmelab (François Zaninotto, 2016), que encadena grep con awk.
Funciona con un único archivo, pero prefiero la versión solo con awk de arriba por dos motivos que pude reproducir tanto en GNU Make 4.4.1 como en el 3.81 de macOS.
En cuanto un Makefile usa include, grep antepone el nombre del archivo a cada coincidencia, y la ayuda de marmelab muestra Makefile y tools.mk donde deberían ir los nombres de los targets.
Su patrón, además, omite los targets con una / o un dígito en el nombre, como test/cover o k8s-up.
El programa awk de arriba muestra todos los targets que tienen una descripción ##, también los de los archivos incluidos y los que tienen una / o un dígito en el nombre.
Para ver un ejemplo real, aquí tienes el Makefile de un plugin de kubectl escrito en Go. Como siguiente paso, consulta un Makefile para ahorrar tiempo en proyectos en Go, de Alex Edwards, que añade targets para auditar, formatear y recargar en caliente.
GNU Make en macOS y Windows
Advertencia
A fecha de octubre de 2026, macOS incluye GNU Make 3.81, publicado en 2006.
Las funcionalidades añadidas desde entonces, como !=, .ONESHELL y .RECIPEPREFIX, fallan sin avisar o se detienen con missing separator, así que un Makefile que funciona en Linux puede comportarse de otra forma en un Mac.
Comprueba qué versión tienes con make --version.
En macOS 26.7.1, con las Command Line Tools 27.0 instaladas, su primera línea es:
$ make --version | head -n 1
GNU Make 3.81En el resto de la salida, el binario aparece como "built for i386-apple-darwin11.3.0". GNU Make 3.81 es la última versión con licencia GPLv2; la 3.82, de julio de 2010, fue la primera con GPLv3, y ese es probablemente el motivo por el que Apple no ha pasado de la 3.81, aunque nunca lo ha confirmado. La última versión del código fuente que ha publicado Apple, gnumake-136 (mayo de 2024), sigue declarando la versión 3.81.
Los fallos silenciosos son los más peligrosos.
En GNU Make 4.0 y posteriores, != asigna a una variable la salida de un comando de shell:
VERSION != echo 1.2.3
SHELL_VERSION := $(shell echo 1.2.3)
show:
@echo "!= gives '$(VERSION)', shell gives '$(SHELL_VERSION)'"$ make
!= gives '1.2.3', shell gives '1.2.3'El make 3.81 de macOS lee la primera línea como una asignación = normal a una variable llamada VERSION !, no da ningún error e imprime != gives '', shell gives '1.2.3'.
$(shell ...) funciona en ambas versiones, así que es la opción portable.
| Funcionalidad | GNU Make 4.4.1 | make 3.81 de macOS |
|---|---|---|
VAR != command (GNU Make 4.0) | Asigna la salida del comando | Define una variable llamada VAR !, sin error |
.ONESHELL: (3.82) | Ejecuta cada recipe en una sola shell | Se ignora sin avisar |
.RECIPEPREFIX (3.82) | Sustituye a la tabulación | missing separator |
| Una línea de recipe que falla | make: *** [Makefile:4: check] Error 1 | make: *** [check] Error 1 |
make -C dir | Imprime Entering directory y Leaving directory | No imprime ninguna sin -w (un cambio en la versión de Apple) |
| Fechas de modificación | Con precisión inferior al segundo, si el sistema de archivos lo permite | Segundos enteros |
| Nombres en los mensajes | 'greeting.txt' | `greeting.txt' |
Aparte de estas diferencias, todos los ejemplos de este post, incluido el target help, se comportan igual en ambas versiones.
Instalar un GNU Make más reciente en macOS
A fecha de octubre de 2026, la fórmula make de Homebrew instala GNU Make 4.4.1 como gmake, así que no sustituye al make del sistema:
brew install make
gmake --versionPara invocarlo como make, pon su directorio gnubin al principio de tu PATH, como sugiere el aviso de la fórmula, por ejemplo en tu ~/.zshrc:
export PATH="$(brew --prefix)/opt/make/libexec/gnubin:$PATH"Si otras personas van a ejecutar tu Makefile en un Mac de serie, no confíes en eso: limítate a las funcionalidades que admite la 3.81.
Make en Windows
Windows no incluye Make, pero tienes varias opciones:
- WSL ejecuta una distribución de Linux, e instalas Make con su gestor de paquetes, en la versión que incluya esa distribución.
- MSYS2 instala GNU Make 4.4.1 (a fecha de octubre de 2026) con
pacman -S make. - Chocolatey instala GNU Make 4.4.1 (a fecha de octubre de 2026) con
choco install make. Es una versión nativa de Windows, así que sus recipes se ejecutan encmd.exesalvo que encuentre unsh.exe, como el de Git Bash, en tuPATH.
Evita el Make de GnuWin32, que todavía encontrarás en respuestas antiguas: es GNU Make 3.81, actualizado por última vez en 2006.
Cuándo usar Make, y cuándo no
Las ventajas de Make son fáciles de enumerar.
Se instala con un paquete en cualquier distribución de Linux y forma parte de las Command Line Tools en macOS, así que la mayoría de desarrolladores ya lo tienen.
Sabe qué archivos están desactualizados, algo que un simple command runner no sabe.
Y un Makefile en la raíz de un repositorio es una convención que todo el mundo reconoce.
Sus inconvenientes son igual de reales: la regla de la tabulación, las diferencias entre versiones, una sintaxis que se vuelve críptica más allá de los casos sencillos, y un lenguaje de recipes que en realidad es shell, lo que lo hace incómodo en Windows.
La portabilidad también es menor de lo que parece.
Los sistemas BSD incluyen su propio make, que comparte la sintaxis de las reglas pero usa directivas distintas (.if, .include), y el estándar POSIX no añadió .PHONY, ?=, += ni -include a su make hasta POSIX.1-2024.
Si todo esto te estorba, estas son las alternativas que yo miraría, a fecha de octubre de 2026:
- just es "un command runner, no un sistema de build", con una sintaxis parecida a la de Make y sin necesidad de
.PHONY. - Task es un task runner que se configura en un archivo YAML,
Taskfile.yml. - Los npm scripts viven en
package.json, y encajan de forma natural en proyectos JavaScript. - Un script de shell basta para dos o tres comandos que nunca necesitan evitar trabajo ya hecho.
- Maven y Gradle ya definen los pasos de build, test y empaquetado de un proyecto Java.
- CMake describe los builds de C y C++ y genera Makefiles (o archivos de Ninja) a partir de ellos.
- Ninja es "un sistema de build pequeño centrado en la velocidad", pensado para que lo genere una herramienta como CMake en lugar de escribirlo a mano.
- Bazel es una herramienta de build y test para bases de código grandes y multilenguaje, "similar a Make, Maven y Gradle".
Preguntas frecuentes
¿Make es solo para C y C++?
No. Make no sabe qué lenguaje está compilando: compara fechas de archivos y ejecuta comandos de shell. Los ejemplos de este post generan archivos de texto y gestionan un proyecto en Go, y los proyectos de Java, Python, documentación e infraestructura lo usan de la misma forma.
¿Qué significa "No rule to make target"?
make: *** No rule to make target 'nope'. Stop. significa que has pedido un target que no es un archivo y que ninguna regla del Makefile puede generar: revisa cómo lo has escrito.
Cuando el archivo que falta es un prerequisite, el mensaje indica la regla que lo necesita, como en make: *** No rule to make target 'missing.txt', needed by 'all'. Stop., así que revisa la ruta de ese archivo.
¿Qué significa "Nothing to be done for"?
make: Nothing to be done for 'all'. significa que el target no tiene recipe propia y que ninguno de sus prerequisites necesitaba regenerarse.
No es un error: Make lo ha comprobado todo y no ha encontrado trabajo que hacer.
¿Makefile, makefile o GNUmakefile?
Usa Makefile.
Reserva GNUmakefile para un archivo que dependa de funcionalidades exclusivas de GNU, ya que otras implementaciones de make no lo buscan.
No mantengas ambos: Make lee GNUmakefile e ignora Makefile.
¿Sigue siendo relevante Make?
Sí.
Unos 50 años después de que Feldman lo escribiera, Make sigue siendo el punto de entrada de innumerables proyectos, y los frameworks actuales siguen apoyándose en él: el quick start de Kubebuilder, por ejemplo, gestiona un nuevo operador de Kubernetes con make install, make run y make deploy.
Los task runners más modernos son agradables de usar, pero un Makefile no obliga a los colaboradores a instalar nada nuevo.
Conclusión
Un Makefile es una lista de reglas: un target, sus prerequisites y una recipe que empieza con una tabulación.
Con esas tres piezas, Make regenera solo lo que ha cambiado, y con .PHONY, unas cuantas variables y un target help, se convierte en el task runner de un proyecto.
Indenta las recipes con tabulaciones de verdad y rara vez volverás a ver missing separator.
Ten presente el 3.81 que incluye macOS y tu Makefile funcionará igual en un Mac.
Cuando necesites más, el manual de GNU Make y makefiletutorial.com cubren lo que este post deja fuera.
Un Makefile da a cada colaborador, humano o agente, un comando estable para compilar, testear y pasar el linter a un proyecto.
