GNU Make nació para compilar programas en C, pero hoy se usa en proyectos de cualquier lenguaje como punto de entrada común: make test, make run o make deploy funcionan igual en todos los equipos y documentan cómo se trabaja con el proyecto. En este tutorial aprenderás la sintaxis de un Makefile, cómo evita repetir trabajo gracias a las dependencias y construirás un Makefile completo para un proyecto Python con Docker Compose en Ubuntu 24.04.

Requisitos previos

Para seguir esta guía necesitas:

  • Un equipo o servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario con privilegios sudo. En macOS, make viene con las herramientas de línea de comandos de Xcode, aunque es una versión antigua (3.81); instala una actual con brew install make y úsala como gmake.
  • Python 3 y el paquete python3-venv para el ejemplo del paso 4.
  • Docker con el plugin de Compose si quieres probar los targets del paso 6 (es opcional).

Paso 1: Instalar make

Instala GNU Make desde los repositorios de Ubuntu:

sudo apt update
sudo apt install make

Comprueba la versión:

make --version | head -n 1
GNU Make 4.3

Paso 2: Escribir tu primer Makefile

Un Makefile está formado por reglas con esta estructura:

target: prerrequisitos
	receta
  • target: normalmente el archivo que se genera, o el nombre de una tarea.
  • prerrequisitos: archivos u otros targets que deben estar actualizados antes.
  • receta: los comandos que se ejecutan. Cada línea debe empezar con un tabulador, no con espacios.

Crea un directorio de pruebas y un Makefile con dos tareas:

mkdir ~/make-demo && cd ~/make-demo
nano Makefile
.PHONY: hello date

hello:
	@echo "Hola desde make"

date:
	date --iso-8601=seconds

Ejecuta las tareas:

make hello
make date
Hola desde make
date --iso-8601=seconds
2026-09-25T11:40:12+00:00

Por defecto, make muestra cada comando antes de ejecutarlo; la @ al principio de la línea lo oculta. Si ejecutas make sin argumentos, se ejecuta el primer target del archivo.

.PHONY declara que hello y date son tareas y no archivos. Sin esa línea, si alguien crea un archivo llamado hello en el directorio, make diría 'hello' is up to date y no haría nada. Declara siempre como .PHONY los targets que no generan un archivo con su nombre.

Paso 3: Usar dependencias entre archivos

La verdadera ventaja de make es que solo rehace lo necesario: compara la fecha de modificación del target con la de sus prerrequisitos y solo ejecuta la receta si alguno es más reciente. Crea unos archivos de ejemplo:

mkdir -p src
echo "uno" > src/a.txt
echo "dos" > src/b.txt

Sustituye el contenido del Makefile por este, que empaqueta src/ en dist/app.tar.gz:

SRC := $(wildcard src/*.txt)

.PHONY: clean

dist/app.tar.gz: $(SRC)
	mkdir -p $(@D)
	tar -czf $@ $^

clean:
	rm -rf dist

Aquí aparecen las variables y las variables automáticas más útiles:

ExpresiónSignificado
VAR := valorAsigna el valor una sola vez, al leer el Makefile
VAR = valorAsignación recursiva: se evalúa cada vez que se usa
VAR ?= valorAsigna solo si la variable no viene ya definida, por ejemplo del entorno
$@Nombre del target (dist/app.tar.gz)
$^Todos los prerrequisitos (src/a.txt src/b.txt)
$<El primer prerrequisito
$(@D)Directorio del target (dist)

Ejecuta make dos veces seguidas:

make
make
mkdir -p dist
tar -czf dist/app.tar.gz src/a.txt src/b.txt
make: 'dist/app.tar.gz' is up to date.

La segunda vez no hace nada porque ningún archivo de src/ ha cambiado. Modifica uno y vuelve a ejecutarlo:

touch src/b.txt
make
mkdir -p dist
tar -czf dist/app.tar.gz src/a.txt src/b.txt

Cualquier variable se puede sobrescribir desde la línea de comandos, por ejemplo make SRC=src/a.txt, algo muy útil para elegir entornos o versiones.

Paso 4: Crear un Makefile para un proyecto Python

Ahora aplicarás todo esto a un caso real: un proyecto Python con entorno virtual, pruebas y linter. El entorno virtual se trata como un archivo más, de modo que se recrea solo cuando cambia requirements.txt.

Instala el módulo de entornos virtuales si no lo tienes:

sudo apt install python3-venv

Crea un proyecto de ejemplo:

mkdir ~/py-demo && cd ~/py-demo
mkdir tests
nano requirements.txt
pytest
ruff

Crea una prueba mínima para que make test tenga algo que ejecutar:

nano tests/test_basic.py
def test_suma():
    assert 1 + 1 == 2

Crea el Makefile:

nano Makefile
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
.DEFAULT_GOAL := help

VENV   := .venv
PYTHON := $(VENV)/bin/python

.PHONY: help install test lint format clean

help: ## Muestra esta ayuda
	@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | \
	  awk 'BEGIN {FS = ":.*?## "}; {printf "  %-10s %s\n", $$1, $$2}'

$(VENV)/.installed: requirements.txt
	python3 -m venv $(VENV)
	$(PYTHON) -m pip install --upgrade pip
	$(PYTHON) -m pip install -r requirements.txt
	touch $@

install: $(VENV)/.installed ## Crea el entorno virtual e instala las dependencias

test: install ## Ejecuta las pruebas
	$(PYTHON) -m pytest -q

lint: install ## Revisa el código con ruff
	$(VENV)/bin/ruff check .

format: install ## Formatea el código con ruff
	$(VENV)/bin/ruff format .

clean: ## Borra el entorno virtual y las cachés
	rm -rf $(VENV) .pytest_cache .ruff_cache
	find . -type d -name __pycache__ -prune -exec rm -rf {} +

Qué hace cada parte de la cabecera:

  • SHELL := bash y .SHELLFLAGS := -eu -o pipefail -c ejecutan las recetas con Bash en modo estricto, así un error en medio de una tubería detiene make.
  • .DELETE_ON_ERROR: borra el target si su receta falla, para que no quede un archivo a medias que parezca válido.
  • .DEFAULT_GOAL := help hace que make sin argumentos muestre la ayuda.

El target $(VENV)/.installed es un archivo marcador: se crea al terminar la instalación y, como depende de requirements.txt, make reinstala las dependencias solo cuando ese archivo cambia. Fíjate también en $$1 y $$2 dentro de awk: en una receta, $ es un carácter especial de make y hay que duplicarlo para que llegue a la shell.

Ejecuta make para ver la ayuda generada a partir de los comentarios ##:

make
  help       Muestra esta ayuda
  install    Crea el entorno virtual e instala las dependencias
  test       Ejecuta las pruebas
  lint       Revisa el código con ruff
  format     Formatea el código con ruff
  clean      Borra el entorno virtual y las cachés

Ejecuta las pruebas. La primera vez creará el entorno virtual:

make test
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip
...
.venv/bin/python -m pip install -r requirements.txt
...
touch .venv/.installed
.venv/bin/python -m pytest -q
.                                                                        [100%]
1 passed in 0.01s

Si lo ejecutas otra vez, solo se lanzan las pruebas, porque el entorno ya está al día.

Paso 5: Encadenar tareas y ejecutarlas en paralelo

Los prerrequisitos también sirven para componer tareas. Añade al final del Makefile un target check, también declarado como .PHONY, que ejecute el linter y las pruebas:

.PHONY: check
check: lint test ## Ejecuta el linter y las pruebas

Cuando las tareas son independientes, -j las ejecuta en paralelo:

make -j2 check

Antes de lanzar un target que no conoces, puedes ver qué comandos ejecutaría sin ejecutarlos con make -n:

make -n clean
rm -rf .venv .pytest_cache .ruff_cache
find . -type d -name __pycache__ -prune -exec rm -rf {} +

Paso 6: Añadir targets para Docker Compose

Si el proyecto se ejecuta con Docker Compose, el Makefile evita recordar opciones largas. Añade estos targets al final del Makefile:

COMPOSE := docker compose

.PHONY: up down logs ps

up: ## Arranca los contenedores en segundo plano
	$(COMPOSE) up -d --build

down: ## Detiene y elimina los contenedores
	$(COMPOSE) down

logs: ## Muestra los logs en tiempo real
	$(COMPOSE) logs -f --tail=100

ps: ## Lista los contenedores del proyecto
	$(COMPOSE) ps

Los nuevos targets aparecen automáticamente en make help gracias a sus comentarios ##. Para usar otro archivo de Compose sin tocar el Makefile, sobrescribe la variable: make up COMPOSE="docker compose -f compose.prod.yaml".

Solución de problemas

Makefile:5: *** missing separator. Stop.: la línea 5 de la receta empieza con espacios en lugar de un tabulador. Si son exactamente ocho espacios, make lo indica con (did you mean TAB instead of 8 spaces?). Muchos editores convierten los tabuladores en espacios; comprueba la línea con cat -A Makefile, donde un tabulador se muestra como ^I.

make: 'test' is up to date. sin ejecutar nada: existe un archivo o directorio con el nombre del target (por ejemplo, la carpeta test/). Declara el target en .PHONY.

Un cd en la receta no tiene efecto en la línea siguiente: cada línea de la receta se ejecuta en una shell distinta. Encadena los comandos en la misma línea, como cd build && ./configure.

Una variable de la shell sale vacía: make ha interpretado el $. Escribe $$VAR para las variables de la shell y $(VAR) para las de make.

Conclusión

Has aprendido la estructura de las reglas de make, cómo evita repetir trabajo con las dependencias entre archivos y cómo usar variables, targets .PHONY y una ayuda autodocumentada. El Makefile del proyecto Python se convierte en la documentación ejecutable de cómo instalar, probar y arrancar el proyecto.

Como siguientes pasos puedes:

  • Llamar a los mismos targets (make check) desde tu sistema de CI para que local y CI ejecuten exactamente lo mismo.
  • Añadir targets de despliegue que invoquen Ansible o rsync con variables por entorno, como make deploy ENV=staging.
  • Consultar el manual de GNU Make (info make) para reglas de patrón como %.o: %.c y funciones como $(patsubst).