--- title: "Cómo automatizar tareas de un proyecto con Makefile" description: "Aprende a escribir un Makefile: reglas, variables, targets .PHONY, ayuda autodocumentada y un flujo completo para un proyecto Python con Docker Compose." category: "Herramientas de Automatización" language: es source: https://cubepath.com/es/docs/herramientas-de-automatización/makefile-para-automatizacion-de-tareas updated: 2026-09-25 --- # Cómo automatizar tareas de un proyecto con Makefile 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: ```bash sudo apt update sudo apt install make ``` Comprueba la versión: ```bash make --version | head -n 1 ``` ```output GNU Make 4.3 ``` ## Paso 2: Escribir tu primer Makefile Un Makefile está formado por reglas con esta estructura: ```makefile 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: ```bash mkdir ~/make-demo && cd ~/make-demo nano Makefile ``` ```makefile .PHONY: hello date hello: @echo "Hola desde make" date: date --iso-8601=seconds ``` Ejecuta las tareas: ```bash make hello make date ``` ```output 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: ```bash 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`: ```makefile 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: ```bash make make ``` ```output 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: ```bash touch src/b.txt make ``` ```output 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: ```bash sudo apt install python3-venv ``` Crea un proyecto de ejemplo: ```bash mkdir ~/py-demo && cd ~/py-demo mkdir tests nano requirements.txt ``` ```text pytest ruff ``` Crea una prueba mínima para que `make test` tenga algo que ejecutar: ```bash nano tests/test_basic.py ``` ```python def test_suma(): assert 1 + 1 == 2 ``` Crea el Makefile: ```bash nano Makefile ``` ```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 `##`: ```bash make ``` ```output 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: ```bash make test ``` ```output 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: ```makefile .PHONY: check check: lint test ## Ejecuta el linter y las pruebas ``` Cuando las tareas son independientes, `-j` las ejecuta en paralelo: ```bash make -j2 check ``` Antes de lanzar un target que no conoces, puedes ver qué comandos ejecutaría sin ejecutarlos con `make -n`: ```bash make -n clean ``` ```output 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: ```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)`.