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,makeviene con las herramientas de línea de comandos de Xcode, aunque es una versión antigua (3.81); instala una actual conbrew install makey úsala comogmake. - Python 3 y el paquete
python3-venvpara 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ón | Significado |
|---|---|
VAR := valor | Asigna el valor una sola vez, al leer el Makefile |
VAR = valor | Asignación recursiva: se evalúa cada vez que se usa |
VAR ?= valor | Asigna 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 := bashy.SHELLFLAGS := -eu -o pipefail -cejecutan 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 := helphace quemakesin 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
rsynccon variables por entorno, comomake deploy ENV=staging. - Consultar el manual de GNU Make (
info make) para reglas de patrón como%.o: %.cy funciones como$(patsubst).
