Makefiles: automatiza tareas repetitivas sin complicar tu proyecto

Makefiles: automatiza tareas repetitivas sin complicar tu proyecto
Photo by Gabriel Heinzer / Unsplash

En muchos proyectos pequeños, los primeros comandos caben perfectamente en la memoria: instalar dependencias, ejecutar pruebas, iniciar el servidor o limpiar archivos generados.

Con el tiempo aparecen variaciones: alguien usa un gestor de paquetes distinto, las pruebas requieren preparar una base de datos o el comando de desarrollo necesita tres variables de entorno. Lo que antes era una línea termina repartido entre mensajes de chat, documentación incompleta y el historial de la terminal.

Un Makefile no sustituye a las herramientas propias de un lenguaje ni pretende ser un sistema de despliegue completo. Su valor está en otro lugar: ofrece una interfaz breve y uniforme para tareas repetitivas.

En lugar de recordar cómo se ejecutan las pruebas, una persona puede ejecutar:

make test

Qué es un Makefile

make nació como una herramienta para compilar software a partir de dependencias. Sigue siendo excelente para eso, pero también se usa como un pequeño catálogo de tareas de proyecto.

Un archivo llamado Makefile define objetivos —targets— y las recetas necesarias para alcanzarlos.

La forma mínima es esta:

objetivo:
	comando

Hay un detalle importante: la línea del comando comienza con un tabulador real, no con espacios. Es uno de los errores más comunes al crear un Makefile a mano.

Si el editor reemplaza los tabuladores por espacios, make normalmente mostrará un error parecido a:

missing separator

Al guardar ese contenido en un archivo llamado Makefile, se puede ejecutar:

make objetivo

Objetivos, dependencias y recetas

Un objetivo puede depender de otros objetivos. En un proyecto que debe instalar dependencias antes de probar, podríamos escribir:

install:
	npm ci

test: install
	npm test

Aquí test depende de install. Antes de ejecutar npm test, make intentará ejecutar el objetivo install.

Para tareas de automatización general conviene distinguir entre archivos reales y acciones. Si existe un archivo o directorio llamado test, make puede pensar que el objetivo ya está actualizado y omitir la receta.

La solución es declarar como .PHONY los objetivos que siempre representan acciones:

.PHONY: install test

install:
	npm ci

test: install
	npm test

En la práctica, la mayoría de los objetivos de un Makefile de aplicación —run, test, lint y clean— deberían ser .PHONY.

Un ejemplo útil para un proyecto web

El siguiente ejemplo no presupone una aplicación concreta. Agrupa comandos habituales de un proyecto JavaScript que ya define scripts en su archivo package.json:

.PHONY: help install run test lint check clean

help:
	@printf "Comandos disponibles:\n"
	@printf "  make install  Instala dependencias bloqueadas\n"
	@printf "  make run      Inicia el entorno de desarrollo\n"
	@printf "  make test     Ejecuta las pruebas\n"
	@printf "  make lint     Ejecuta el linter\n"
	@printf "  make check    Ejecuta pruebas y linter\n"
	@printf "  make clean    Elimina artefactos locales seguros\n"

install:
	npm ci

run:
	npm run dev

test:
	npm test

lint:
	npm run lint

check: test lint

clean:
	rm -rf coverage .cache

make help funciona como la puerta de entrada del proyecto. El prefijo @ evita que make imprima el comando printf antes de su resultado, lo que hace la salida más clara.

El objetivo check no tiene una receta propia: simplemente agrupa test y lint.

El objetivo clean merece cuidado. Borrar coverage o una caché local suele ser seguro; borrar node_modules, datos de desarrollo o archivos de configuración puede no serlo.

Una buena regla es que un comando de limpieza sea idempotente: debe poder ejecutarse dos veces sin producir un error ni borrar datos que una persona no esperaba perder.

Variables para evitar repetición

Las variables hacen que un Makefile sea más fácil de adaptar. Por ejemplo, si un proyecto permite elegir el puerto de desarrollo:

PORT ?= 3000

.PHONY: run

run:
	PORT=$(PORT) npm run dev

El operador ?= define un valor solo si no existe uno previo. Así se mantiene un valor práctico por defecto y se deja una salida para casos puntuales:

make run PORT=4173

No todas las configuraciones deberían vivir dentro del Makefile. Las credenciales, los tokens y otros valores privados deben continuar en el mecanismo de secretos del proyecto, como un gestor de secretos o un archivo local que no se incluya en Git.

El Makefile puede consumir una variable de entorno ya disponible, pero no debe contener secretos.

También es útil definir los nombres de las herramientas que se repiten:

PACKAGE_MANAGER ?= npm

install:
	$(PACKAGE_MANAGER) ci

Este patrón tiene sentido cuando el equipo realmente necesita la variación. Añadir variables «por si acaso» puede volver un archivo pequeño más difícil de leer que los comandos que buscaba simplificar.

Diseña comandos que una persona quiera ejecutar

Un buen Makefile describe tareas, no trucos de shell. Los nombres deben expresar intención:

  • make test, no make all-the-tests-now.
  • make format, no make prettier-and-eslint-fix.
  • make db-reset, solo si deja claro que afecta datos de desarrollo.
  • make deploy, únicamente si incorpora las comprobaciones y protecciones apropiadas.

La consistencia importa más que la cantidad de objetivos. Es preferible tener cinco comandos previsibles que veinte objetivos con nombres ambiguos.

El objetivo predeterminado es el primero del archivo. Por eso es habitual colocar help al principio: ejecutar solo make muestra qué puede hacer el proyecto.

Si el proyecto necesita un flujo de verificación estándar, también puede ser razonable que el primer objetivo sea check. Depende de qué acción sea más segura y frecuente.

Errores comunes

Usar espacios donde make espera un tabulador

Las recetas necesitan tabuladores. Configura el editor para que el Makefile conserve los tabuladores y revisa ese carácter si aparece el error missing separator.

Mezclar dependencias de archivos con tareas de aplicación

make compara las fechas de los archivos para decidir si debe reconstruirlos. Eso es perfecto al compilar, pero puede sorprender en un objetivo como test.

Declara .PHONY cuando el nombre representa una acción.

Crear dependencias implícitas

Si make test requiere que exista una base de datos, que las migraciones estén aplicadas o que un servicio esté activo, el objetivo debe prepararlo explícitamente o documentarlo con claridad.

Un comando que funciona únicamente en el equipo de quien lo escribió no es una buena interfaz.

Convertir Make en una capa innecesaria

Si el proyecto ya usa npm run, poetry, just, task o los scripts de un framework y esos comandos son claros, quizá no haga falta sumar make.

Úsalo cuando reduzca la fricción entre herramientas o cree un vocabulario común para el equipo.

Cuándo usarlo y cuándo no

Un Makefile encaja especialmente bien cuando un repositorio mezcla varios lenguajes, contenedores, pruebas y herramientas de calidad.

Por ejemplo, make check puede ejecutar comandos de Python, Node, Docker o una utilidad del sistema sin obligar a cada persona a recordar el orden.

No es la única opción. Un proyecto puramente Node puede estar perfectamente servido por los scripts de package.json; uno en Python puede preferir tareas de tox, nox o poe; y un flujo complejo de infraestructura puede necesitar una herramienta específica.

El criterio es simple: elige la interfaz que haga que las tareas correctas sean más fáciles de descubrir y ejecutar.

Checklist para incorporarlo

Antes de añadir un Makefile a un repositorio existente, revisa esta lista:

  1. Identifica los tres a cinco comandos que el equipo repite con mayor frecuencia.
  2. Conserva los comandos nativos debajo del objetivo; make debe ser una interfaz, no una abstracción opaca.
  3. Declara como .PHONY las tareas que no producen archivos.
  4. Incluye make help o documenta el objetivo predeterminado.
  5. Evita secretos y operaciones destructivas silenciosas.
  6. Ejecuta cada objetivo desde un clon limpio antes de pedirle a otra persona que lo use.

Un Makefile pequeño no hace magia. Lo que sí hace es convertir conocimiento disperso en comandos cortos, documentados y repetibles.

Esa mejora se nota desde el primer make test que alguien puede ejecutar sin tener que preguntar cómo funciona el proyecto.