Un logotipo con el texto blog.marcnuri.com
English
Inicio»Herramientas»¿Qué es un Makefile? Tutorial Práctico de Make con Ejemplos

Entradas Recientes

  • ¡Fabric8 Kubernetes Client 8.0 está disponible!
  • ¡Fabric8 Kubernetes Client 7.9 está disponible!
  • ¡Eclipse JKube 1.20 está disponible!
  • Project, Agent, Person: El Eje que Falta en Herramientas de Programación con IA
  • ¡Fabric8 Kubernetes Client 7.8 está disponible!

Categorías

  • Antiguo
  • Cloud Native
  • Desarrollo Backend
  • Desarrollo Frontend
  • Herramientas
  • Ingeniería de calidad
  • Inteligencia Artificial
  • JavaScript
  • Operaciones
  • Personal
  • Proyectos personales
  • Reflexiones sobre Ingeniería

Archivos

  • septiembre 2026
  • julio 2026
  • junio 2026
  • mayo 2026
  • abril 2026
  • marzo 2026
  • febrero 2026
  • enero 2026
  • diciembre 2025
  • octubre 2025
  • septiembre 2025
  • julio 2025
  • mayo 2025
  • abril 2025
  • marzo 2025
  • febrero 2025
  • enero 2025
  • diciembre 2024
  • noviembre 2024
  • agosto 2024
  • junio 2024
  • mayo 2024
  • abril 2024
  • marzo 2024
  • febrero 2024
  • enero 2024
  • diciembre 2023
  • noviembre 2023
  • octubre 2023
  • septiembre 2023
  • agosto 2023
  • julio 2023
  • junio 2023
  • mayo 2023
  • abril 2023
  • marzo 2023
  • febrero 2023
  • enero 2023
  • diciembre 2022
  • noviembre 2022
  • octubre 2022
  • septiembre 2022
  • agosto 2022
  • julio 2022
  • junio 2022
  • mayo 2022
  • marzo 2022
  • febrero 2022
  • enero 2022
  • diciembre 2021
  • noviembre 2021
  • octubre 2021
  • septiembre 2021
  • agosto 2021
  • julio 2021
  • enero 2021
  • diciembre 2020
  • octubre 2020
  • septiembre 2020
  • agosto 2020
  • junio 2020
  • mayo 2020
  • marzo 2020
  • febrero 2020
  • enero 2020
  • noviembre 2019
  • septiembre 2019
  • julio 2019
  • diciembre 2018
  • agosto 2018
  • julio 2018
  • junio 2018
  • mayo 2018
  • marzo 2018
  • febrero 2018
  • noviembre 2017
  • octubre 2017
  • agosto 2017
  • julio 2017
  • enero 2017
  • julio 2016
  • mayo 2016
  • enero 2016
  • diciembre 2015
  • noviembre 2015
  • diciembre 2014
  • noviembre 2014
  • octubre 2014
  • marzo 2014
  • agosto 2012
  • febrero 2011
  • enero 2010
  • junio 2008
  • mayo 2008
  • abril 2008
  • enero 2008
  • junio 2007
  • mayo 2007
  • abril 2007
  • marzo 2007

¿Qué es un Makefile? Tutorial Práctico de Make con Ejemplos

2010-01-02 en Herramientas etiquetado Build / Automatización / Shell / Linux por Marc Nuri | Última actualización: 2026-11-01
English version

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:

Makefile
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:

bash
$ make
echo "Hello, Make"
Hello, Make

Make 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:

Makefile
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: hello no 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:

Salida
Makefile:2: *** missing separator (did you mean TAB instead of 8 spaces?).  Stop.

Con menos espacios, obtienes el mensaje a secas:

Salida
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:

bash
$ 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:

.editorconfig
[{Makefile,*.mk}]
indent_style = tab

Si prefieres no usar tabulaciones en absoluto, GNU Make 3.82 y posteriores te permiten elegir otro prefijo para las recipes con .RECIPEPREFIX:

Makefile
.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:

Makefile
greeting.txt: name.txt
	sed 's/^/Hello, /' name.txt > greeting.txt

Crea name.txt con echo Marc > name.txt y ejecuta make. greeting.txt todavía no existe, así que Make ejecuta la recipe:

bash
$ make
sed 's/^/Hello, /' name.txt > greeting.txt
$ cat greeting.txt
Hello, Marc

Ejecuta make de nuevo y, como greeting.txt ya es más reciente que name.txt, no hay nada que hacer:

bash
$ 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:

bash
$ touch name.txt
$ make
sed 's/^/Hello, /' name.txt > greeting.txt

Make 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:

Makefile
shout.txt: greeting.txt
	tr a-z A-Z < greeting.txt > shout.txt
greeting.txt: name.txt
	sed 's/^/Hello, /' name.txt > greeting.txt

Ahora make genera shout.txt, el nuevo primer target, y un cambio en name.txt regenera ambos archivos, en orden:

bash
$ touch name.txt
$ make
sed 's/^/Hello, /' name.txt > greeting.txt
tr a-z A-Z < greeting.txt > shout.txt
$ cat shout.txt
HELLO, MARC

Un 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:

Makefile
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:

bash
$ 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:

Makefile
.PHONY: docs
docs:
	@echo "Generating the docs"
bash
$ make docs
Generating the docs

La 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:

Makefile
LAZY = Hello, $(NAME)
EAGER := Hello, $(NAME)
NAME = Make
show:
	@echo "lazy=[$(LAZY)] eager=[$(EAGER)]"
bash
$ make
lazy=[Hello, Make] eager=[Hello, ]
  • = define una variable de expansión recursiva. Make guarda Hello, $(NAME) tal cual y lo expande cada vez que se usa la variable, y para entonces NAME ya está definida.
  • := define una variable de expansión simple. Make expande el valor una sola vez, donde se define, y en ese punto NAME todaví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.

Makefile
NAME ?= World
hello:
	@echo "Hello, $(NAME)"
bash
$ make
Hello, World
$ make NAME=Marc
Hello, Marc
$ NAME=Env make
Hello, Env

Un 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:

Makefile
NAME = World
hello:
	@echo "Hello, $(NAME)"
bash
$ NAME=Env make
Hello, World
$ NAME=Env make -e
Hello, Env
$ make NAME=Marc
Hello, Marc

La 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:

Makefile
TEST_FLAGS = -v
TEST_FLAGS += -race
test:
	@echo "go test $(TEST_FLAGS) ./..."
bash
$ 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:

Makefile
ifeq ($(shell uname -s),Darwin)
OPEN := open
else
OPEN := xdg-open
endif

En 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:

Makefile
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:

bash
$ 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.txt

El 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:

bash
$ 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
VariableContieneEn el ejemplo
$@El targetbuild/page.txt
$<El primer prerequisiteheader.txt
$^Todos los prerequisites, sin duplicadosheader.txt body.txt
$?Los prerequisites más recientes que el targetAmbos en la primera ejecución, body.txt después de touch body.txt
$(@D)La parte de directorio del targetbuild

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:

Makefile
%.upper: %.txt
	tr a-z A-Z < $< > $@
bash
$ 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:

Makefile
where:
	cd /
	pwd
	cd / && pwd
bash
$ 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:

Makefile
where:
	cd / && \
	pwd

Este 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:

Makefile
.ONESHELL:
where:
	cd /
	pwd

En 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 $$:

Makefile
home:
	@echo "Make variable: $(HOME)"
	@echo "Shell variable: $$HOME"
	@for f in a b; do echo "Item $$f"; done
bash
$ 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:

Makefile
check:
	-false
	@echo "Still running"
	false
	@echo "Never printed"
bash
$ make
false
make: [Makefile:2: check] Error 1 (ignored)
Still running
false
make: *** [Makefile:4: check] Error 1

Make 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ónQué hace
-nImprime los comandos que ejecutaría, sin ejecutarlos (ejecución en seco o dry run)
-j NEjecuta hasta N recipes a la vez; -j sin número no pone límite
-C dirCambia a dir antes de leer el Makefile
-f fileLee file en lugar de GNUmakefile, makefile o Makefile
-BRegenera todos los targets, incluso los que están actualizados
VAR=valueDefine una variable que prevalece sobre el Makefile y el entorno
-ePermite 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:

Makefile
build:
	@echo "Building"
	go build -o hello .
bash
$ 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:

Makefile
all: slow fast
slow:
	@sleep 1; echo "slow done"
fast:
	@echo "fast done"
bash
$ 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:

bash
$ 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:

Makefile
.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):

bash
$ make

Usage:
  make <target>
  help                   Show this help
  build                  Build the binary
  test                   Run the tests
  clean                  Remove the binary

El resto de targets ejecutan los comandos de Go:

bash
$ make build
go build -o hello .
$ ./hello
Hello, Make
bash
$ make test
go test ./...
ok  	marcnuri.com/hello	0.001s
bash
$ make BINARY=greeter build
go build -o greeter .
$ make clean
rm -f hello

Cada parte del Makefile usa algo de las secciones anteriores:

  • .DEFAULT_GOAL := help hace que un simple make imprima la ayuda en lugar de generar el primer target. Está disponible desde GNU Make 3.81, así que también funciona en macOS.
  • BINARY ?= hello es un valor por defecto que la línea de comandos sobrescribe, como muestra make BINARY=greeter build.
  • .PHONY incluye todos los targets, porque ninguno produce un archivo con su propio nombre.
  • El comentario ## ... tras cada target es su descripción. La recipe de help ejecuta awk sobre $(MAKEFILE_LIST), la lista de todos los Makefiles que Make ha leído, e imprime cada línea target: ## description que 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:

bash
$ make --version | head -n 1
GNU Make 3.81

En 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:

Makefile
VERSION != echo 1.2.3
SHELL_VERSION := $(shell echo 1.2.3)
show:
	@echo "!= gives '$(VERSION)', shell gives '$(SHELL_VERSION)'"
bash
$ 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.

FuncionalidadGNU Make 4.4.1make 3.81 de macOS
VAR != command (GNU Make 4.0)Asigna la salida del comandoDefine una variable llamada VAR !, sin error
.ONESHELL: (3.82)Ejecuta cada recipe en una sola shellSe ignora sin avisar
.RECIPEPREFIX (3.82)Sustituye a la tabulaciónmissing separator
Una línea de recipe que fallamake: *** [Makefile:4: check] Error 1make: *** [check] Error 1
make -C dirImprime Entering directory y Leaving directoryNo imprime ninguna sin -w (un cambio en la versión de Apple)
Fechas de modificaciónCon precisión inferior al segundo, si el sistema de archivos lo permiteSegundos 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:

bash
brew install make
gmake --version

Para 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:

bash
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 en cmd.exe salvo que encuentre un sh.exe, como el de Git Bash, en tu PATH.

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.

Icono de TwitterIcono de FacebookIcono de LinkedInIcono de PinterestIcono de correo electrónico

Navegador de artículos
Windows XP: Cómo habilitar la cuenta de Administrador/Administrator¿Qué es una Immediately Invoked Function Expression (IIFE)?
© 2007 - 2026 Marc Nuri