Alcides Ramos Cordero

Software Engineer

Usando ficheros Makefile

Tal y como vimos en el post sobre Uso avanzado de Docker, a veces tenemos que introducir los mismos comandos una y otra vez mientras trabajamos en nuestros proyectos. Veamos cómo ganar agilidad mediante el uso de ficheros Makefile y el comando make.

Introducción

¿Qué es un fichero Makefile?

Un fichero Makefile es un archivo que contiene un conjunto de directivas que permiten automatizar tareas, especialmente durante la fase de desarrollo de software.

Diferencias con otras opciones

En la actualidad podemos hacer uso de múltiples mecanismos para automatizar tareas:

Pero este tipo de opciones no dejan de ser soluciones individuales, que dependen de las preferencias de cada usuario y, por tanto, limitadas a la máquina donde se instalan o configuran.

Ahora bien, cuando trabajamos en equipo debemos cambiar el foco y buscar opciones transversales que causen muy poco impacto a cada usuario, que sean fáciles de usar y que no impliquen reconfigurar parcial o total el entorno de cada colaborador. Y es aquí donde los ficheros Makefile entran en juego ya que toda distribución UNIX o Linux viene con el comando make pre-instalado y, por tanto, sólo tenemos que preocuparnos de definir los comandos que vamos a necesitar.

Usando ficheros Makefile

Make es una herramienta de gestión de dependencias usada en entornos UNIX y permite compilar nuestra aplicación o bien, ejecutar comandos de consola de una forma automatizada.

Así pues, make usa el fichero Makefile en el path actual como entrada. Para ver la ayuda de este comando podemos usar el parámetro --help:

$ make --help

Una ventaja del comando make es que permite el autocompletado de comandos de nuestro Makefile

Creando nuestro propio Makefile

Existen algunas secciones específicas de los ficheros Makefile tales como .PHONY y .DEFAULT_GOAL que definen el comportamiento por defecto de nuestro fichero Makefile. Así pues, si no especificamos ningún target mostrará el especificado en .DEFAULT_GOAL.

Algunos consejos rápidos para la creación de ficheros Makefile:

Plantilla genérica

.PHONY: no_targets__ info help build deploy doc
        no_targets__:

.DEFAULT_GOAL := help

#-----------------------------------------------------------------
# CONSTANTS
#-----------------------------------------------------------------

ifneq (,$(findstring xterm,${TERM}))
	BLACK   := $(shell tput -Txterm setaf 0)
	RED     := $(shell tput -Txterm setaf 1)
	GREEN   := $(shell tput -Txterm setaf 2)
	YELLOW  := $(shell tput -Txterm setaf 3)
	BLUE    := $(shell tput -Txterm setaf 4)
	MAGENTA := $(shell tput -Txterm setaf 5)
	CYAN    := $(shell tput -Txterm setaf 6)
	WHITE   := $(shell tput -Txterm setaf 7)
	RESET   := $(shell tput -Txterm sgr0)
else
	BLACK   := ""
	RED     := ""
	GREEN   := ""
	YELLOW  := ""
	BLUE    := ""
	MAGENTA := ""
	CYAN    := ""
	WHITE   := ""
	RESET   := ""
endif

COMMAND_COLOR := $(YELLOW)

#-----------------------------------------------------------------
# HELP
#-----------------------------------------------------------------

help:
	@echo "╔══════════════════════════════════════════════════════════════════════════════╗"
	@echo "║                                                                              ║"
	@echo "║                           ${CYAN}.:${RESET} AVAILABLE COMMANDS ${CYAN}:.${RESET}                           ║"
	@echo "║                                                                              ║"
	@echo "╚══════════════════════════════════════════════════════════════════════════════╝"
	@echo ""
	@grep -E '^[a-zA-Z_0-9%-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "· ${TARGET_COLOR}%-30s${RESET} %s\n", $$1, $$2}'
	@echo ""

#-----------------------------------------------------------------
# COMMANDS
#-----------------------------------------------------------------

priv-1:
	@command step #1
	@command step #2
	...

priv-2:
	command step #1

public: priv-1 priv-2 ## Command summary
    command step #1
	...

El comando make help mostrará, de nuestro fichero Makefile, todos los comandos que tengan en su definición un comentario que empiece por dos almohadillas (##).

Esto nos permite tener comandos no expuestos públicamente (comandos privados o auxiliares) y comandos públicos con una breve información descriptiva

Comentarios

Linea Descripción
.DEFAULT_GOAL := help Aquí indicamos el comportamiento por defecto del comando make sin parámetro alguno. Por defecto que muestre la ayuda contextual de los comandos públicos del Makefile
COMMAND_COLOR := $(YELLOW) Indicamos el color que deseamos para el listado de nuestros comandos
public: priv-1 priv-2 Para desarrollar comandos que realicen uno o varios comandos atómicos, se recomienda crear esos comandos atómicos de manera privada y, en el comando principal, añadirlos como dependencia. De este modo se ejecutará, por orden de aparición, los pasos de los diferentes comandos atómicos y, por último, los del comando principal.

Comandos make frecuentes para aplicaciones PHP

Comando Descripción
build Construye el contenedor
rebuild Reconstruye el contenedor
up Arranca el contenedor
down Apaga el contenedor
restart Reinicia el contenedor
logs Visualiza los logs del contenedor
info Visualiza la información de los servicios de la aplicación
bash Accedemos mediante bash al contenedor de servicio
composer-install Instala las dependencias en entorno local
composer-install-no-dev Instala las dependencias en entorno de producción
composer-update Actualiza las dependencias de la aplicación
composer-dump-auto Actualiza la carga de clases de la aplicación
tests Ejecuta los tests de la aplicación
tests-debug Ejecuta los tests de la aplicación en modo depuración
cache-clear Elimina la cache de nuestra aplicación

Makefile resultante

.PHONY: no_targets__ info help build deploy doc
        no_targets__:

.DEFAULT_GOAL := help

#-----------------------------------------------------------------
# CONSTANTS
#-----------------------------------------------------------------

ifneq (,$(findstring xterm,${TERM}))
	BLACK   := $(shell tput -Txterm setaf 0)
	RED     := $(shell tput -Txterm setaf 1)
	GREEN   := $(shell tput -Txterm setaf 2)
	YELLOW  := $(shell tput -Txterm setaf 3)
	BLUE    := $(shell tput -Txterm setaf 4)
	MAGENTA := $(shell tput -Txterm setaf 5)
	CYAN    := $(shell tput -Txterm setaf 6)
	WHITE   := $(shell tput -Txterm setaf 7)
	RESET   := $(shell tput -Txterm sgr0)
else
	BLACK   := ""
	RED     := ""
	GREEN   := ""
	YELLOW  := ""
	BLUE    := ""
	MAGENTA := ""
	CYAN    := ""
	WHITE   := ""
	RESET   := ""
endif

TARGET_COLOR := $(YELLOW)

SERVICE_NAME = api

#-----------------------------------------------------------------
# HELP
#-----------------------------------------------------------------

help:
	@echo "╔══════════════════════════════════════════════════════════════════════════════╗"
	@echo "║                                                                              ║"
	@echo "║                           ${CYAN}.:${RESET} AVAILABLE COMMANDS ${CYAN}:.${RESET}                           ║"
	@echo "║                                                                              ║"
	@echo "╚══════════════════════════════════════════════════════════════════════════════╝"
	@echo ""
	@grep -E '^[a-zA-Z_0-9%-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "· ${TARGET_COLOR}%-30s${RESET} %s\n", $$1, $$2}'
	@echo ""

#-----------------------------------------------------------------
# MISCELANEOUS
#-----------------------------------------------------------------

check-is-required-%:
	@if [ -z "$($(*))" ] ; then \
		echo "" ; \
		echo " ⛔  ${RED}Parameter [ ${WHITE}${*}${RED} ] is required!${RESET}" ; \
		echo "" ; \
		exit 1 ; \
	fi;

#-----------------------------------------------------------------
# DOCKER-COMPOSE RELATED
#-----------------------------------------------------------------

build: ## Builds the container(s)
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ BUILDING THE SERVICE(s)                                                     │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
    @docker-compose build
	@echo " 📦  ${GREEN}Build done!${RESET}"
	@echo ""

build: ## Builds the container(s)
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ BUILDING THE SERVICE(s)                                                     │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose build
	@echo " 📦  ${GREEN}Service has been built!${RESET}"
	@echo ""

rebuild: down build ## Rebuilds the container(s)
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ REBUILDING THE SERVICE(s)                                                   │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose up --remove-orphans
	@echo " 📦  ${CYAN}Service has been rebuilt!${RESET}"
	@echo ""

up: check-folder ## Starts the container(s) in daemonized mode
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ BUILDING, (RE)CREATING, STARTING AND ATTACHING TO CONTAINERS FOR A SERVICE  │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose up --remove-orphans -d
	@echo ""
	@echo " 🟢  ${GREEN}Service is up & running!${RESET}"
	@echo ""

down: ## Stops the container(s)
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ STOPPING AND REMOVING CONTAINERS, NETWORKS, VOLUMES AND IMAGES              │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose down
	@echo ""
	@echo " 🟠  ${YELLOW}Service has been stopped!${RESET}"
	@echo ""

restart: ## Restarts the container(s)
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ RESTARTING ALL STOPPED AND RUNNING SERVICES                                 │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose restart
	@echo ""
	@echo " 🔵  ${BLUE}Service has been restarted!${RESET}"
	@echo ""

logs: ## Prints out the container(s) log(s)
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ DISPLAYING LOG OUTPUT FROM SERVICES                                         │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose logs
	@echo ""
	@echo " 🔎  ${WHITE}Service log were displayed!${RESET}"
	@echo ""

#-----------------------------------------------------------------
# SERVICE RELATED
#-----------------------------------------------------------------

bash: ## Opens a bash connection to main service
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ GETTING AN INTERACTIVE PROMPT FROM SERVICE CONTAINER                        │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose exec ${SERVICE_NAME} bash
	@echo ""
	@echo " ✅  ${GREEN}Task is done!${RESET}"
	@echo ""

info: ## Displays information about main service
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ DISPLAYING GENERIC INFORMATION FROM SERVICE                                 │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose exec ${SERVICE_NAME} bash -c "php --version"
	@echo ""
	@echo " ✅  ${GREEN}Task is done!${RESET}"
	@echo ""

#-----------------------------------------------------------------
# COMPOSER RELATED
#-----------------------------------------------------------------

composer-install-no-dev: ## Performs <composer install --optimize-autoloader --no-dev> onto main service
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ INSTALLING DEPENDENCIES ONTO APPLICATION SERVICE IN PRODUCTION MODE         │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose exec ${SERVICE_NAME} bash -c "COMPOSER_MEMORY_LIMIT=-1 composer install --optimize-autoloader --no-dev"
	@echo ""

composer-install: ## Performs <composer install --optimize-autoloader> onto main service
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ INSTALLING DEPENDENCIES ONTO APPLICATION SERVICE                            │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose exec ${SERVICE_NAME} bash -c "COMPOSER_MEMORY_LIMIT=-1 composer install --optimize-autoloader"
	@echo ""
	@echo " ✅  ${GREEN}Task is done!${RESET}"
	@echo ""

composer-update: ## Performs <composer update> onto main service
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ UPDATING DEPENDENCIES ONTO APPLICATION SERVICE                              │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose exec ${SERVICE_NAME} bash -c "COMPOSER_MEMORY_LIMIT=-1 composer update"
	@echo ""
	@echo " ✅  ${GREEN}Task is done!${RESET}"
	@echo ""

composer-dump-auto: ## Performs <composer dump-auto> onto main service
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ GENERATING OPTIMIZED AUTOLOAD CLASS MAP ONTO APPLICATION SERVICE            │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose exec ${SERVICE_NAME} bash -c "COMPOSER_MEMORY_LIMIT=-1 composer dump-auto"
	@echo ""
	@echo " ✅  ${GREEN}Task is done!${RESET}"
	@echo ""

#-----------------------------------------------------------------
# APPLICATION RELATED
#-----------------------------------------------------------------

tests: ## Executes the PHPUnit testsuite onto main service
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ RUNNING THE TESTSUITE ONTO APPLICATION SERVICE                              │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose exec ${SERVICE_NAME} bash -c "cd /code/api && ./vendor/bin/phpunit"
	@echo ""
	@echo " ✅  ${GREEN}Task is done!${RESET}"
	@echo ""

tests-debug: ## Executes the PHPUnit testsuite with debug flag
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ RUNNING THE TESTSUITE ONTO APPLICATION SERVICE IN DEBUG MODE                │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose exec ${SERVICE_NAME} bash -c "cd /code/api && ./vendor/bin/phpunit --debug"
	@echo ""
	@echo " ✅  ${GREEN}Task is done!${RESET}"
	@echo ""

cache-clear: ## Performs <php artisan clear:cache> onto app container
	@echo "┌─────────────────────────────────────────────────────────────────────────────┐"
	@echo "│ CLEARING THE APPLICATION CACHE FROM APPLICATION SERVICE                     │"
	@echo "└─────────────────────────────────────────────────────────────────────────────┘"
	@echo ""
	@docker-compose exec ${SERVICE_NAME} bash -c "cd /code/api && php artisan cache:clear"
	@echo ""
	@echo " ✅  ${GREEN}Task is done!${RESET}"
	@echo ""

Descarga

Puedes descargar el fichero de ejemplo desde aquí.

Versión del documento

[^v1.0]: Última Modificación: 03/03/2021